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:
parent
4f6f4bfae9
commit
05337ff232
@ -121,6 +121,7 @@
|
|||||||
"prosemirror-view": "^0.12.0",
|
"prosemirror-view": "^0.12.0",
|
||||||
"react": "^15.1.0",
|
"react": "^15.1.0",
|
||||||
"react-addons-css-transition-group": "^15.3.1",
|
"react-addons-css-transition-group": "^15.3.1",
|
||||||
|
"react-autosuggest": "^7.0.1",
|
||||||
"react-datetime": "^2.6.0",
|
"react-datetime": "^2.6.0",
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
"react-hot-loader": "^3.0.0-beta.2",
|
"react-hot-loader": "^3.0.0-beta.2",
|
||||||
|
@ -27,10 +27,6 @@ export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
|
|||||||
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
|
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
|
||||||
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE';
|
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)
|
* Simple Action Creators (Internal)
|
||||||
* We still need to export them for tests
|
* We still need to export them for tests
|
||||||
@ -122,35 +118,6 @@ export function emmptyDraftCreated(entry) {
|
|||||||
payload: 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
|
* 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 SelectPreview from './Widgets/SelectPreview';
|
||||||
import ObjectControl from './Widgets/ObjectControl';
|
import ObjectControl from './Widgets/ObjectControl';
|
||||||
import ObjectPreview from './Widgets/ObjectPreview';
|
import ObjectPreview from './Widgets/ObjectPreview';
|
||||||
|
import RelationControl from './Widgets/RelationControl';
|
||||||
|
import RelationPreview from './Widgets/RelationPreview';
|
||||||
|
|
||||||
|
|
||||||
registry.registerWidget('string', StringControl, StringPreview);
|
registry.registerWidget('string', StringControl, StringPreview);
|
||||||
registry.registerWidget('text', TextControl, TextPreview);
|
registry.registerWidget('text', TextControl, TextPreview);
|
||||||
@ -32,6 +35,7 @@ registry.registerWidget('date', DateControl, DatePreview);
|
|||||||
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||||
registry.registerWidget('select', SelectControl, SelectPreview);
|
registry.registerWidget('select', SelectControl, SelectPreview);
|
||||||
registry.registerWidget('object', ObjectControl, ObjectPreview);
|
registry.registerWidget('object', ObjectControl, ObjectPreview);
|
||||||
|
registry.registerWidget('relation', RelationControl, RelationPreview);
|
||||||
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
|
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
|
||||||
|
|
||||||
export function resolveWidget(name) { // eslint-disable-line
|
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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectSearchedEntries } from '../reducers';
|
import { selectSearchedEntries } from '../reducers';
|
||||||
import { searchEntries } from '../actions/entries';
|
import { searchEntries, clearSearch } from '../actions/search';
|
||||||
import { Loader } from '../components/UI';
|
import { Loader } from '../components/UI';
|
||||||
import EntryListing from '../components/EntryListing/EntryListing';
|
import EntryListing from '../components/EntryListing/EntryListing';
|
||||||
import styles from './breakpoints.css';
|
import styles from './breakpoints.css';
|
||||||
@ -12,6 +12,7 @@ class SearchPage extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
isFetching: PropTypes.bool,
|
isFetching: PropTypes.bool,
|
||||||
searchEntries: PropTypes.func.isRequired,
|
searchEntries: PropTypes.func.isRequired,
|
||||||
|
clearSearch: PropTypes.func.isRequired,
|
||||||
searchTerm: PropTypes.string.isRequired,
|
searchTerm: PropTypes.string.isRequired,
|
||||||
collections: ImmutablePropTypes.seq,
|
collections: ImmutablePropTypes.seq,
|
||||||
entries: ImmutablePropTypes.list,
|
entries: ImmutablePropTypes.list,
|
||||||
@ -30,9 +31,13 @@ class SearchPage extends React.Component {
|
|||||||
searchEntries(nextProps.searchTerm);
|
searchEntries(nextProps.searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.clearSearch();
|
||||||
|
}
|
||||||
|
|
||||||
handleLoadMore = (page) => {
|
handleLoadMore = (page) => {
|
||||||
const { searchTerm, searchEntries } = this.props;
|
const { searchTerm, searchEntries } = this.props;
|
||||||
searchEntries(searchTerm, page);
|
if (!isNaN(page)) searchEntries(searchTerm, page);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -70,5 +75,8 @@ function mapStateToProps(state, ownProps) {
|
|||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
{ searchEntries }
|
{
|
||||||
|
searchEntries,
|
||||||
|
clearSearch,
|
||||||
|
}
|
||||||
)(SearchPage);
|
)(SearchPage);
|
||||||
|
@ -33,6 +33,66 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global {
|
: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 {
|
& .rdt {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,13 @@ import {
|
|||||||
ENTRY_SUCCESS,
|
ENTRY_SUCCESS,
|
||||||
ENTRIES_REQUEST,
|
ENTRIES_REQUEST,
|
||||||
ENTRIES_SUCCESS,
|
ENTRIES_SUCCESS,
|
||||||
SEARCH_ENTRIES_REQUEST,
|
|
||||||
SEARCH_ENTRIES_SUCCESS,
|
|
||||||
} from '../actions/entries';
|
} from '../actions/entries';
|
||||||
|
|
||||||
|
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
|
||||||
|
|
||||||
let collection;
|
let collection;
|
||||||
let loadedEntries;
|
let loadedEntries;
|
||||||
let page;
|
let page;
|
||||||
let searchTerm;
|
|
||||||
|
|
||||||
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
||||||
switch (action.type) {
|
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:
|
case SEARCH_ENTRIES_SUCCESS:
|
||||||
loadedEntries = action.payload.entries;
|
loadedEntries = action.payload.entries;
|
||||||
page = action.payload.page;
|
|
||||||
searchTerm = action.payload.searchTerm;
|
|
||||||
return state.withMutations((map) => {
|
return state.withMutations((map) => {
|
||||||
loadedEntries.forEach(entry => (
|
loadedEntries.forEach(entry => (
|
||||||
map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false))
|
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:
|
default:
|
||||||
@ -82,9 +64,4 @@ export const selectEntries = (state, collection) => {
|
|||||||
return slugs && slugs.map(slug => selectEntry(state, collection, slug));
|
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;
|
export default entries;
|
||||||
|
@ -6,6 +6,7 @@ import entries, * as fromEntries from './entries';
|
|||||||
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||||
import entryDraft from './entryDraft';
|
import entryDraft from './entryDraft';
|
||||||
import collections from './collections';
|
import collections from './collections';
|
||||||
|
import search from './search';
|
||||||
import medias, * as fromMedias from './medias';
|
import medias, * as fromMedias from './medias';
|
||||||
import globalUI from './globalUI';
|
import globalUI from './globalUI';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ const reducers = {
|
|||||||
auth,
|
auth,
|
||||||
config,
|
config,
|
||||||
collections,
|
collections,
|
||||||
|
search,
|
||||||
integrations,
|
integrations,
|
||||||
editor,
|
editor,
|
||||||
entries,
|
entries,
|
||||||
@ -33,8 +35,10 @@ export const selectEntry = (state, collection, slug) =>
|
|||||||
export const selectEntries = (state, collection) =>
|
export const selectEntries = (state, collection) =>
|
||||||
fromEntries.selectEntries(state.entries, collection);
|
fromEntries.selectEntries(state.entries, collection);
|
||||||
|
|
||||||
export const selectSearchedEntries = state =>
|
export const selectSearchedEntries = (state) => {
|
||||||
fromEntries.selectSearchedEntries(state.entries);
|
const searchItems = state.search.get('entryIds');
|
||||||
|
return searchItems && searchItems.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug));
|
||||||
|
};
|
||||||
|
|
||||||
export const selectUnpublishedEntry = (state, status, slug) =>
|
export const selectUnpublishedEntry = (state, status, slug) =>
|
||||||
fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, 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;
|
51
yarn.lock
51
yarn.lock
@ -1201,12 +1201,6 @@ braces@^1.8.2:
|
|||||||
preserve "^0.2.0"
|
preserve "^0.2.0"
|
||||||
repeat-element "^1.1.2"
|
repeat-element "^1.1.2"
|
||||||
|
|
||||||
bricks.js@^1.7.0:
|
|
||||||
version "1.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/bricks.js/-/bricks.js-1.7.0.tgz#2863bde2f03cd48d41dcca88bea1a198c839f608"
|
|
||||||
dependencies:
|
|
||||||
knot.js "^1.1.1"
|
|
||||||
|
|
||||||
brorand@^1.0.1:
|
brorand@^1.0.1:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5"
|
||||||
@ -4676,10 +4670,6 @@ kind-of@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-buffer "^1.0.2"
|
is-buffer "^1.0.2"
|
||||||
|
|
||||||
knot.js@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/knot.js/-/knot.js-1.1.1.tgz#35dc900d3c62813f0ca119c3d6a0a598e5cb6896"
|
|
||||||
|
|
||||||
known-css-properties@^0.0.5:
|
known-css-properties@^0.0.5:
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.0.5.tgz#33de5b8279010a72db917d33119e4c27c078490a"
|
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.0.5.tgz#33de5b8279010a72db917d33119e4c27c078490a"
|
||||||
@ -6934,6 +6924,22 @@ react-addons-test-utils@^15.3.2:
|
|||||||
version "15.3.2"
|
version "15.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6"
|
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.3.2.tgz#c09a44f583425a4a9c1b38444d7a6c3e6f0f41f6"
|
||||||
|
|
||||||
|
react-autosuggest:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-7.0.1.tgz#e751d2c2e516a344f6cdc150672e85f134f5f2f1"
|
||||||
|
dependencies:
|
||||||
|
react-autowhatever "^7.0.0"
|
||||||
|
react-redux "^4.4.5"
|
||||||
|
redux "^3.6.0"
|
||||||
|
shallow-equal "^1.0.0"
|
||||||
|
|
||||||
|
react-autowhatever@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-7.0.0.tgz#7ea19f8024183acf1568fc8e4b76c0d0cc250d00"
|
||||||
|
dependencies:
|
||||||
|
react-themeable "^1.1.0"
|
||||||
|
section-iterator "^2.0.0"
|
||||||
|
|
||||||
react-css-themr@~1.4.1:
|
react-css-themr@~1.4.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-css-themr/-/react-css-themr-1.4.1.tgz#26fa63fe0a8f7343b019f088f88475ca89da2d5a"
|
resolved "https://registry.yarnpkg.com/react-css-themr/-/react-css-themr-1.4.1.tgz#26fa63fe0a8f7343b019f088f88475ca89da2d5a"
|
||||||
@ -7054,6 +7060,15 @@ react-redux@^4.4.0:
|
|||||||
lodash "^4.2.0"
|
lodash "^4.2.0"
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
|
react-redux@^4.4.5:
|
||||||
|
version "4.4.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-4.4.6.tgz#4b9d32985307a11096a2dd61561980044fcc6209"
|
||||||
|
dependencies:
|
||||||
|
hoist-non-react-statics "^1.0.3"
|
||||||
|
invariant "^2.0.0"
|
||||||
|
lodash "^4.2.0"
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
react-router-redux@^4.0.5:
|
react-router-redux@^4.0.5:
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.6.tgz#10cf98dce911d7dd912a05bdb07fee4d3c563dee"
|
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.6.tgz#10cf98dce911d7dd912a05bdb07fee4d3c563dee"
|
||||||
@ -7090,6 +7105,12 @@ react-sortable@^1.2.0:
|
|||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849"
|
resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849"
|
||||||
|
|
||||||
|
react-themeable@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e"
|
||||||
|
dependencies:
|
||||||
|
object-assign "^3.0.0"
|
||||||
|
|
||||||
react-toolbox@^1.2.1:
|
react-toolbox@^1.2.1:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-toolbox/-/react-toolbox-1.2.2.tgz#ae8f3290da9e053625df97a63df7224943b79679"
|
resolved "https://registry.yarnpkg.com/react-toolbox/-/react-toolbox-1.2.2.tgz#ae8f3290da9e053625df97a63df7224943b79679"
|
||||||
@ -7279,7 +7300,7 @@ redux-thunk@^1.0.3:
|
|||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-1.0.3.tgz#778aa0099eea0595031ab6b39165f6670d8d26bd"
|
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-1.0.3.tgz#778aa0099eea0595031ab6b39165f6670d8d26bd"
|
||||||
|
|
||||||
redux@^3.2.0, redux@^3.3.1, redux@^3.5.2:
|
redux@^3.2.0, redux@^3.3.1, redux@^3.5.2, redux@^3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"
|
resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7557,6 +7578,10 @@ sax@^1.1.4, sax@~1.2.1:
|
|||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||||
|
|
||||||
|
section-iterator@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a"
|
||||||
|
|
||||||
selection-position@^1.0.0:
|
selection-position@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/selection-position/-/selection-position-1.0.0.tgz#e43f87151d94957efa170e10e02c901b47f703c7"
|
resolved "https://registry.yarnpkg.com/selection-position/-/selection-position-1.0.0.tgz#e43f87151d94957efa170e10e02c901b47f703c7"
|
||||||
@ -7640,6 +7665,10 @@ sha.js@2.2.6:
|
|||||||
version "2.2.6"
|
version "2.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba"
|
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba"
|
||||||
|
|
||||||
|
shallow-equal@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7"
|
||||||
|
|
||||||
shallowequal@0.2.x:
|
shallowequal@0.2.x:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"
|
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user