Relation search widget (#186)
* search action/reducer refactor * Relation widget skeleton * search clearing * query action + reducer * Autocomplete component for RelationControl
This commit is contained in:
@ -27,10 +27,6 @@ export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
|
||||
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
|
||||
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE';
|
||||
|
||||
export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST';
|
||||
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
|
||||
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
* We still need to export them for tests
|
||||
@ -122,35 +118,6 @@ export function emmptyDraftCreated(entry) {
|
||||
payload: entry,
|
||||
};
|
||||
}
|
||||
|
||||
export function searchingEntries(searchTerm) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_REQUEST,
|
||||
payload: { searchTerm },
|
||||
};
|
||||
}
|
||||
|
||||
export function searchSuccess(searchTerm, entries, page) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
searchTerm,
|
||||
entries,
|
||||
page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function searchFailure(searchTerm, error) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_FAILURE,
|
||||
payload: {
|
||||
searchTerm,
|
||||
error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported simple Action Creators
|
||||
*/
|
||||
@ -244,23 +211,3 @@ export function persistEntry(collection, entryDraft) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function searchEntries(searchTerm, page = 0) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
let collections = state.collections.keySeq().toArray();
|
||||
collections = collections.filter(collection => selectIntegration(state, collection, 'search'));
|
||||
const integration = selectIntegration(state, collections[0], 'search');
|
||||
if (!integration) {
|
||||
dispatch(searchFailure(searchTerm, 'Search integration is not configured.'));
|
||||
}
|
||||
const provider = integration ?
|
||||
getIntegrationProvider(state.integrations, integration)
|
||||
: currentBackend(state.config);
|
||||
dispatch(searchingEntries(searchTerm));
|
||||
provider.search(collections, searchTerm, page).then(
|
||||
response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)),
|
||||
error => dispatch(searchFailure(searchTerm, error))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
137
src/actions/search.js
Normal file
137
src/actions/search.js
Normal file
@ -0,0 +1,137 @@
|
||||
import { currentBackend } from '../backends/backend';
|
||||
import { getIntegrationProvider } from '../integrations';
|
||||
import { selectIntegration } from '../reducers';
|
||||
|
||||
/*
|
||||
* Contant Declarations
|
||||
*/
|
||||
export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST';
|
||||
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
|
||||
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
|
||||
|
||||
export const QUERY_REQUEST = 'QUERY_REQUEST';
|
||||
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
|
||||
export const QUERY_FAILURE = 'QUERY_FAILURE';
|
||||
|
||||
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
* We still need to export them for tests
|
||||
*/
|
||||
export function searchingEntries(searchTerm) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_REQUEST,
|
||||
payload: { searchTerm },
|
||||
};
|
||||
}
|
||||
|
||||
export function searchSuccess(searchTerm, entries, page) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
searchTerm,
|
||||
entries,
|
||||
page,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function searchFailure(searchTerm, error) {
|
||||
return {
|
||||
type: SEARCH_ENTRIES_FAILURE,
|
||||
payload: {
|
||||
searchTerm,
|
||||
error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function querying(collection, searchFields, searchTerm) {
|
||||
return {
|
||||
type: QUERY_REQUEST,
|
||||
payload: {
|
||||
collection,
|
||||
searchFields,
|
||||
searchTerm,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function querySuccess(collection, searchFields, searchTerm, response) {
|
||||
return {
|
||||
type: QUERY_SUCCESS,
|
||||
payload: {
|
||||
collection,
|
||||
searchFields,
|
||||
searchTerm,
|
||||
response,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function queryFailure(collection, searchFields, searchTerm, error) {
|
||||
return {
|
||||
type: QUERY_SUCCESS,
|
||||
payload: {
|
||||
collection,
|
||||
searchFields,
|
||||
searchTerm,
|
||||
error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Exported simple Action Creators
|
||||
*/
|
||||
|
||||
export function clearSearch() {
|
||||
return { type: SEARCH_CLEAR };
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Exported Thunk Action Creators
|
||||
*/
|
||||
|
||||
// SearchEntries will search for complete entries in all collections.
|
||||
export function searchEntries(searchTerm, page = 0) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
let collections = state.collections.keySeq().toArray();
|
||||
collections = collections.filter(collection => selectIntegration(state, collection, 'search'));
|
||||
const integration = selectIntegration(state, collections[0], 'search');
|
||||
if (!integration) {
|
||||
dispatch(searchFailure(searchTerm, 'Search integration is not configured.'));
|
||||
}
|
||||
const provider = integration ?
|
||||
getIntegrationProvider(state.integrations, integration)
|
||||
: currentBackend(state.config);
|
||||
dispatch(searchingEntries(searchTerm));
|
||||
provider.search(collections, searchTerm, page).then(
|
||||
response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)),
|
||||
error => dispatch(searchFailure(searchTerm, error))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Instead of searching for complete entries, query will search for specific fields
|
||||
// in specific collections and return raw data (no entries).
|
||||
export function query(collection, searchFields, searchTerm) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const integration = selectIntegration(state, collection, 'search');
|
||||
if (!integration) {
|
||||
dispatch(searchFailure(searchTerm, 'Search integration is not configured.'));
|
||||
}
|
||||
const provider = integration ?
|
||||
getIntegrationProvider(state.integrations, integration)
|
||||
: currentBackend(state.config);
|
||||
dispatch(querying(collection, searchFields, searchTerm));
|
||||
provider.searchBy(searchFields, collection, searchTerm).then(
|
||||
response => dispatch(querySuccess(collection, searchFields, searchTerm, response)),
|
||||
error => dispatch(queryFailure(collection, searchFields, searchTerm, error))
|
||||
);
|
||||
};
|
||||
}
|
@ -21,6 +21,9 @@ import SelectControl from './Widgets/SelectControl';
|
||||
import SelectPreview from './Widgets/SelectPreview';
|
||||
import ObjectControl from './Widgets/ObjectControl';
|
||||
import ObjectPreview from './Widgets/ObjectPreview';
|
||||
import RelationControl from './Widgets/RelationControl';
|
||||
import RelationPreview from './Widgets/RelationPreview';
|
||||
|
||||
|
||||
registry.registerWidget('string', StringControl, StringPreview);
|
||||
registry.registerWidget('text', TextControl, TextPreview);
|
||||
@ -32,6 +35,7 @@ registry.registerWidget('date', DateControl, DatePreview);
|
||||
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||
registry.registerWidget('select', SelectControl, SelectPreview);
|
||||
registry.registerWidget('object', ObjectControl, ObjectPreview);
|
||||
registry.registerWidget('relation', RelationControl, RelationPreview);
|
||||
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
|
||||
|
||||
export function resolveWidget(name) { // eslint-disable-line
|
||||
|
108
src/components/Widgets/RelationControl.js
Normal file
108
src/components/Widgets/RelationControl.js
Normal file
@ -0,0 +1,108 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
import { Loader } from '../../components/UI/index';
|
||||
import { query, clearSearch } from '../../actions/search';
|
||||
|
||||
|
||||
function escapeRegexCharacters(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
class RelationControl extends Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
field: PropTypes.node,
|
||||
isFetching: PropTypes.bool,
|
||||
query: PropTypes.func.isRequired,
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
queryHits: PropTypes.array, // eslint-disable-line
|
||||
};
|
||||
|
||||
onChange = (event, { newValue }) => {
|
||||
this.props.onChange(newValue);
|
||||
};
|
||||
|
||||
onSuggestionsFetchRequested = debounce(({ value }) => {
|
||||
if (value.length < 3) return;
|
||||
const { field } = this.props;
|
||||
const collection = field.get('collection');
|
||||
const searchFields = field.get('searchFields').map(f => `data.${ f }`).toJS();
|
||||
this.props.query(collection, searchFields, value);
|
||||
}, 80);
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.clearSearch();
|
||||
};
|
||||
|
||||
getMatchingHits = (value) => {
|
||||
const { field, queryHits } = this.props;
|
||||
const searchFields = field.get('searchFields').toJS();
|
||||
const escapedValue = escapeRegexCharacters(value.trim());
|
||||
const regex = new RegExp(`^ ${ escapedValue }`, 'i');
|
||||
|
||||
if (escapedValue === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return queryHits.filter((hit) => {
|
||||
let testResult = false;
|
||||
searchFields.forEach((f) => {
|
||||
testResult = testResult || regex.test(hit.data[f]);
|
||||
});
|
||||
return testResult;
|
||||
});
|
||||
};
|
||||
|
||||
getSuggestionValue = (suggestion) => {
|
||||
const { field } = this.props;
|
||||
const valueField = field.get('valueField');
|
||||
return suggestion.data[valueField];
|
||||
};
|
||||
|
||||
renderSuggestion = (suggestion) => {
|
||||
const { field } = this.props;
|
||||
const valueField = field.get('valueField');
|
||||
return <span>{suggestion.data[valueField]}</span>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, isFetching, queryHits } = this.props;
|
||||
|
||||
const inputProps = {
|
||||
placeholder: '',
|
||||
value: value || '',
|
||||
onChange: this.onChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Autosuggest
|
||||
suggestions={queryHits}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} // eslint-disable-line
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested} // eslint-disable-line
|
||||
getSuggestionValue={this.getSuggestionValue}
|
||||
renderSuggestion={this.renderSuggestion}
|
||||
inputProps={inputProps}
|
||||
/>
|
||||
<Loader active={isFetching} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const isFetching = state.search.get('isFetching');
|
||||
const queryHits = state.search.get('queryHits');
|
||||
return { isFetching, queryHits };
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
query,
|
||||
clearSearch,
|
||||
}
|
||||
)(RelationControl);
|
10
src/components/Widgets/RelationPreview.js
Normal file
10
src/components/Widgets/RelationPreview.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import previewStyle from './defaultPreviewStyle';
|
||||
|
||||
export default function RelationPreview({ value }) {
|
||||
return <div style={previewStyle}>{ value }</div>;
|
||||
}
|
||||
|
||||
RelationPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
};
|
@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectSearchedEntries } from '../reducers';
|
||||
import { searchEntries } from '../actions/entries';
|
||||
import { searchEntries, clearSearch } from '../actions/search';
|
||||
import { Loader } from '../components/UI';
|
||||
import EntryListing from '../components/EntryListing/EntryListing';
|
||||
import styles from './breakpoints.css';
|
||||
@ -12,6 +12,7 @@ class SearchPage extends React.Component {
|
||||
static propTypes = {
|
||||
isFetching: PropTypes.bool,
|
||||
searchEntries: PropTypes.func.isRequired,
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
searchTerm: PropTypes.string.isRequired,
|
||||
collections: ImmutablePropTypes.seq,
|
||||
entries: ImmutablePropTypes.list,
|
||||
@ -30,9 +31,13 @@ class SearchPage extends React.Component {
|
||||
searchEntries(nextProps.searchTerm);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearSearch();
|
||||
}
|
||||
|
||||
handleLoadMore = (page) => {
|
||||
const { searchTerm, searchEntries } = this.props;
|
||||
searchEntries(searchTerm, page);
|
||||
if (!isNaN(page)) searchEntries(searchTerm, page);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -70,5 +75,8 @@ function mapStateToProps(state, ownProps) {
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ searchEntries }
|
||||
{
|
||||
searchEntries,
|
||||
clearSearch,
|
||||
}
|
||||
)(SearchPage);
|
||||
|
@ -33,6 +33,66 @@ h1 {
|
||||
}
|
||||
|
||||
:global {
|
||||
|
||||
& .react-autosuggest__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .react-autosuggest__input {
|
||||
width: 240px;
|
||||
height: 30px;
|
||||
padding: 10px 20px;
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
& .react-autosuggest__input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
& .react-autosuggest__container--open .react-autosuggest__input {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
& .react-autosuggest__suggestions-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-autosuggest__container--open .react-autosuggest__suggestions-container {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 51px;
|
||||
width: 100%;
|
||||
border: 1px solid #aaa;
|
||||
background-color: #fff;
|
||||
font-family: Helvetica, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
& .react-autosuggest__suggestions-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
& .react-autosuggest__suggestion {
|
||||
cursor: pointer;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
& .react-autosuggest__suggestion--focused {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
|
||||
& .rdt {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -4,14 +4,13 @@ import {
|
||||
ENTRY_SUCCESS,
|
||||
ENTRIES_REQUEST,
|
||||
ENTRIES_SUCCESS,
|
||||
SEARCH_ENTRIES_REQUEST,
|
||||
SEARCH_ENTRIES_SUCCESS,
|
||||
} from '../actions/entries';
|
||||
|
||||
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
|
||||
|
||||
let collection;
|
||||
let loadedEntries;
|
||||
let page;
|
||||
let searchTerm;
|
||||
|
||||
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
||||
switch (action.type) {
|
||||
@ -43,29 +42,12 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
||||
}));
|
||||
});
|
||||
|
||||
case SEARCH_ENTRIES_REQUEST:
|
||||
if (action.payload.searchTerm !== state.getIn(['search', 'term'])) {
|
||||
return state.withMutations((map) => {
|
||||
map.setIn(['search', 'isFetching'], true);
|
||||
map.setIn(['search', 'term'], action.payload.searchTerm);
|
||||
});
|
||||
}
|
||||
return state;
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS:
|
||||
loadedEntries = action.payload.entries;
|
||||
page = action.payload.page;
|
||||
searchTerm = action.payload.searchTerm;
|
||||
return state.withMutations((map) => {
|
||||
loadedEntries.forEach(entry => (
|
||||
map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false))
|
||||
));
|
||||
const ids = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })));
|
||||
map.set('search', Map({
|
||||
page,
|
||||
term: searchTerm,
|
||||
ids: page === 0 ? ids : map.getIn(['search', 'ids'], List()).concat(ids),
|
||||
}));
|
||||
});
|
||||
|
||||
default:
|
||||
@ -82,9 +64,4 @@ export const selectEntries = (state, collection) => {
|
||||
return slugs && slugs.map(slug => selectEntry(state, collection, slug));
|
||||
};
|
||||
|
||||
export const selectSearchedEntries = (state) => {
|
||||
const searchItems = state.getIn(['search', 'ids']);
|
||||
return searchItems && searchItems.map(({ collection, slug }) => selectEntry(state, collection, slug));
|
||||
};
|
||||
|
||||
export default entries;
|
||||
|
@ -6,6 +6,7 @@ import entries, * as fromEntries from './entries';
|
||||
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||
import entryDraft from './entryDraft';
|
||||
import collections from './collections';
|
||||
import search from './search';
|
||||
import medias, * as fromMedias from './medias';
|
||||
import globalUI from './globalUI';
|
||||
|
||||
@ -13,6 +14,7 @@ const reducers = {
|
||||
auth,
|
||||
config,
|
||||
collections,
|
||||
search,
|
||||
integrations,
|
||||
editor,
|
||||
entries,
|
||||
@ -33,8 +35,10 @@ export const selectEntry = (state, collection, slug) =>
|
||||
export const selectEntries = (state, collection) =>
|
||||
fromEntries.selectEntries(state.entries, collection);
|
||||
|
||||
export const selectSearchedEntries = state =>
|
||||
fromEntries.selectSearchedEntries(state.entries);
|
||||
export const selectSearchedEntries = (state) => {
|
||||
const searchItems = state.search.get('entryIds');
|
||||
return searchItems && searchItems.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug));
|
||||
};
|
||||
|
||||
export const selectUnpublishedEntry = (state, status, slug) =>
|
||||
fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug);
|
||||
|
67
src/reducers/search.js
Normal file
67
src/reducers/search.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { Map, List } from 'immutable';
|
||||
|
||||
import {
|
||||
SEARCH_ENTRIES_REQUEST,
|
||||
SEARCH_ENTRIES_SUCCESS,
|
||||
QUERY_REQUEST,
|
||||
QUERY_SUCCESS,
|
||||
SEARCH_CLEAR,
|
||||
} from '../actions/search';
|
||||
|
||||
let loadedEntries;
|
||||
let response;
|
||||
let page;
|
||||
let searchTerm;
|
||||
|
||||
const defaultState = Map({ isFetching: false, term: null, page: 0, entryIds: [], queryHits: [] });
|
||||
|
||||
const entries = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case SEARCH_CLEAR:
|
||||
return defaultState;
|
||||
|
||||
case SEARCH_ENTRIES_REQUEST:
|
||||
if (action.payload.searchTerm !== state.get('term')) {
|
||||
return state.withMutations((map) => {
|
||||
map.set('isFetching', true);
|
||||
map.set('term', action.payload.searchTerm);
|
||||
});
|
||||
}
|
||||
return state;
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS:
|
||||
loadedEntries = action.payload.entries;
|
||||
page = action.payload.page;
|
||||
searchTerm = action.payload.searchTerm;
|
||||
return state.withMutations((map) => {
|
||||
const entryIds = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })));
|
||||
map.set('isFetching', false);
|
||||
map.set('page', page);
|
||||
map.set('term', searchTerm);
|
||||
map.set('entryIds', page === 0 ? entryIds : map.get('entryIds', List()).concat(entryIds));
|
||||
});
|
||||
|
||||
case QUERY_REQUEST:
|
||||
if (action.payload.searchTerm !== state.get('term')) {
|
||||
return state.withMutations((map) => {
|
||||
map.set('isFetching', true);
|
||||
map.set('term', action.payload.searchTerm);
|
||||
});
|
||||
}
|
||||
return state;
|
||||
|
||||
case QUERY_SUCCESS:
|
||||
searchTerm = action.payload.searchTerm;
|
||||
response = action.payload.response;
|
||||
return state.withMutations((map) => {
|
||||
map.set('isFetching', false);
|
||||
map.set('term', searchTerm);
|
||||
map.set('queryHits', response.hits);
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default entries;
|
Reference in New Issue
Block a user