feat: improve search to target single collections (#3760)
This commit is contained in:
parent
72596bbbec
commit
588622adb2
78
cypress/integration/search_suggestion_spec.js
Normal file
78
cypress/integration/search_suggestion_spec.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { login } from '../utils/steps';
|
||||||
|
|
||||||
|
const search = (term, collection) => {
|
||||||
|
cy.get('[class*=SearchInput]').clear({ force: true });
|
||||||
|
cy.get('[class*=SearchInput]').type(term, { force: true });
|
||||||
|
cy.get('[class*=SuggestionsContainer]').within(() => {
|
||||||
|
cy.contains(collection).click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertSearchHeading = title => {
|
||||||
|
cy.get('[class*=SearchResultHeading]').should('have.text', title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertSearchResult = (text, collection) => {
|
||||||
|
cy.get('[class*=ListCardLink]').within(() => {
|
||||||
|
if (collection) {
|
||||||
|
cy.contains('h2', collection);
|
||||||
|
}
|
||||||
|
cy.contains('h2', text);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotInSearch = text => {
|
||||||
|
cy.get('[class*=ListCardLink]').within(() => {
|
||||||
|
cy.contains('h2', text).should('not.exist');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Search Suggestion', () => {
|
||||||
|
before(() => {
|
||||||
|
Cypress.config('defaultCommandTimeout', 4000);
|
||||||
|
cy.task('setupBackend', { backend: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.task('teardownBackend', { backend: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
login();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can search in all collections', () => {
|
||||||
|
search('this', 'All Collections');
|
||||||
|
|
||||||
|
assertSearchHeading('Search Results for "this"');
|
||||||
|
|
||||||
|
assertSearchResult('This is post # 20', 'Posts');
|
||||||
|
assertSearchResult('This is a TOML front matter post', 'Posts');
|
||||||
|
assertSearchResult('This is a JSON front matter post', 'Posts');
|
||||||
|
assertSearchResult('This is a YAML front matter post', 'Posts');
|
||||||
|
assertSearchResult('This FAQ item # 5', 'FAQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can search in posts collection', () => {
|
||||||
|
search('this', 'Posts');
|
||||||
|
|
||||||
|
assertSearchHeading('Search Results for "this" in Posts');
|
||||||
|
|
||||||
|
assertSearchResult('This is post # 20');
|
||||||
|
assertSearchResult('This is a TOML front matter post');
|
||||||
|
assertSearchResult('This is a JSON front matter post');
|
||||||
|
assertSearchResult('This is a YAML front matter post');
|
||||||
|
|
||||||
|
assertNotInSearch('This FAQ item # 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can search in faq collection', () => {
|
||||||
|
search('this', 'FAQ');
|
||||||
|
|
||||||
|
assertSearchHeading('Search Results for "this" in FAQ');
|
||||||
|
|
||||||
|
assertSearchResult('This FAQ item # 5');
|
||||||
|
|
||||||
|
assertNotInSearch('This is post # 20');
|
||||||
|
});
|
||||||
|
});
|
@ -19,7 +19,7 @@ describe('search', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
it('should search entries using integration', async () => {
|
it('should search entries in all collections using integration', async () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
search: fromJS({}),
|
search: fromJS({}),
|
||||||
@ -39,6 +39,7 @@ describe('search', () => {
|
|||||||
type: 'SEARCH_ENTRIES_REQUEST',
|
type: 'SEARCH_ENTRIES_REQUEST',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['posts', 'pages'],
|
||||||
page: 0,
|
page: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -46,6 +47,7 @@ describe('search', () => {
|
|||||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['posts', 'pages'],
|
||||||
entries: response.entries,
|
entries: response.entries,
|
||||||
page: response.pagination,
|
page: response.pagination,
|
||||||
},
|
},
|
||||||
@ -55,7 +57,45 @@ describe('search', () => {
|
|||||||
expect(integration.search).toHaveBeenCalledWith(['posts', 'pages'], 'find me', 0);
|
expect(integration.search).toHaveBeenCalledWith(['posts', 'pages'], 'find me', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should search entries using backend', async () => {
|
it('should search entries in a subset of collections using integration', async () => {
|
||||||
|
const store = mockStore({
|
||||||
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
|
search: fromJS({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
selectIntegration.mockReturnValue('search_integration');
|
||||||
|
currentBackend.mockReturnValue({});
|
||||||
|
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||||
|
const integration = { search: jest.fn().mockResolvedValue(response) };
|
||||||
|
getIntegrationProvider.mockReturnValue(integration);
|
||||||
|
|
||||||
|
await store.dispatch(searchEntries('find me', ['pages']));
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions).toHaveLength(2);
|
||||||
|
|
||||||
|
expect(actions[0]).toEqual({
|
||||||
|
type: 'SEARCH_ENTRIES_REQUEST',
|
||||||
|
payload: {
|
||||||
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['pages'],
|
||||||
|
page: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(actions[1]).toEqual({
|
||||||
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
|
payload: {
|
||||||
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['pages'],
|
||||||
|
entries: response.entries,
|
||||||
|
page: response.pagination,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(integration.search).toHaveBeenCalledTimes(1);
|
||||||
|
expect(integration.search).toHaveBeenCalledWith(['pages'], 'find me', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search entries in all collections using backend', async () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
search: fromJS({}),
|
search: fromJS({}),
|
||||||
@ -74,6 +114,7 @@ describe('search', () => {
|
|||||||
type: 'SEARCH_ENTRIES_REQUEST',
|
type: 'SEARCH_ENTRIES_REQUEST',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['posts', 'pages'],
|
||||||
page: 0,
|
page: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -81,6 +122,7 @@ describe('search', () => {
|
|||||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['posts', 'pages'],
|
||||||
entries: response.entries,
|
entries: response.entries,
|
||||||
page: response.pagination,
|
page: response.pagination,
|
||||||
},
|
},
|
||||||
@ -93,10 +135,47 @@ describe('search', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore identical search', async () => {
|
it('should search entries in a subset of collections using backend', async () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
search: fromJS({ isFetching: true, term: 'find me' }),
|
search: fromJS({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||||
|
const backend = { search: jest.fn().mockResolvedValue(response) };
|
||||||
|
currentBackend.mockReturnValue(backend);
|
||||||
|
|
||||||
|
await store.dispatch(searchEntries('find me', ['pages']));
|
||||||
|
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions).toHaveLength(2);
|
||||||
|
|
||||||
|
expect(actions[0]).toEqual({
|
||||||
|
type: 'SEARCH_ENTRIES_REQUEST',
|
||||||
|
payload: {
|
||||||
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['pages'],
|
||||||
|
page: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(actions[1]).toEqual({
|
||||||
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
|
payload: {
|
||||||
|
searchTerm: 'find me',
|
||||||
|
searchCollections: ['pages'],
|
||||||
|
entries: response.entries,
|
||||||
|
page: response.pagination,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(backend.search).toHaveBeenCalledTimes(1);
|
||||||
|
expect(backend.search).toHaveBeenCalledWith([fromJS({ name: 'pages' })], 'find me');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore identical search in all collections', async () => {
|
||||||
|
const store = mockStore({
|
||||||
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
|
search: fromJS({ isFetching: true, term: 'find me', collections: ['posts', 'pages'] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.dispatch(searchEntries('find me'));
|
await store.dispatch(searchEntries('find me'));
|
||||||
@ -104,5 +183,34 @@ describe('search', () => {
|
|||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions).toHaveLength(0);
|
expect(actions).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore identical search in a subset of collections', async () => {
|
||||||
|
const store = mockStore({
|
||||||
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
|
search: fromJS({ isFetching: true, term: 'find me', collections: ['pages'] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.dispatch(searchEntries('find me', ['pages']));
|
||||||
|
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not ignore same search term in different search collections', async () => {
|
||||||
|
const store = mockStore({
|
||||||
|
collections: fromJS({ posts: { name: 'posts' }, pages: { name: 'pages' } }),
|
||||||
|
search: fromJS({ isFetching: true, term: 'find me', collections: ['pages'] }),
|
||||||
|
});
|
||||||
|
const backend = { search: jest.fn().mockResolvedValue({}) };
|
||||||
|
currentBackend.mockReturnValue(backend);
|
||||||
|
|
||||||
|
await store.dispatch(searchEntries('find me', ['posts', 'pages']));
|
||||||
|
|
||||||
|
expect(backend.search).toHaveBeenCalledTimes(1);
|
||||||
|
expect(backend.search).toHaveBeenCalledWith(
|
||||||
|
[fromJS({ name: 'posts' }), fromJS({ name: 'pages' })],
|
||||||
|
'find me',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import history from 'Routing/history';
|
import history from 'Routing/history';
|
||||||
import { getCollectionUrl, getNewEntryUrl } from 'Lib/urlHelper';
|
import { getCollectionUrl, getNewEntryUrl } from 'Lib/urlHelper';
|
||||||
|
|
||||||
export function searchCollections(query) {
|
export function searchCollections(query, collection) {
|
||||||
history.push(`/search/${query}`);
|
if (collection) {
|
||||||
|
history.push(`/collections/${collection}/search/${query}`);
|
||||||
|
} else {
|
||||||
|
history.push(`/search/${query}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showCollection(collectionName) {
|
export function showCollection(collectionName) {
|
||||||
|
@ -5,6 +5,7 @@ import { currentBackend } from '../backend';
|
|||||||
import { getIntegrationProvider } from '../integrations';
|
import { getIntegrationProvider } from '../integrations';
|
||||||
import { selectIntegration } from '../reducers';
|
import { selectIntegration } from '../reducers';
|
||||||
import { EntryValue } from '../valueObjects/Entry';
|
import { EntryValue } from '../valueObjects/Entry';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Constant Declarations
|
* Constant Declarations
|
||||||
@ -23,18 +24,24 @@ export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
|||||||
* Simple Action Creators (Internal)
|
* Simple Action Creators (Internal)
|
||||||
* We still need to export them for tests
|
* We still need to export them for tests
|
||||||
*/
|
*/
|
||||||
export function searchingEntries(searchTerm: string, page: number) {
|
export function searchingEntries(searchTerm: string, searchCollections: string[], page: number) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_ENTRIES_REQUEST,
|
type: SEARCH_ENTRIES_REQUEST,
|
||||||
payload: { searchTerm, page },
|
payload: { searchTerm, searchCollections, page },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchSuccess(searchTerm: string, entries: EntryValue[], page: number) {
|
export function searchSuccess(
|
||||||
|
searchTerm: string,
|
||||||
|
searchCollections: string[],
|
||||||
|
entries: EntryValue[],
|
||||||
|
page: number,
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_ENTRIES_SUCCESS,
|
type: SEARCH_ENTRIES_SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
searchCollections,
|
||||||
entries,
|
entries,
|
||||||
page,
|
page,
|
||||||
},
|
},
|
||||||
@ -124,12 +131,16 @@ export function clearSearch() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// SearchEntries will search for complete entries in all collections.
|
// SearchEntries will search for complete entries in all collections.
|
||||||
export function searchEntries(searchTerm: string, page = 0) {
|
export function searchEntries(
|
||||||
|
searchTerm: string,
|
||||||
|
searchCollections: string[] | null = null,
|
||||||
|
page = 0,
|
||||||
|
) {
|
||||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { search } = state;
|
const { search } = state;
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
const allCollections = state.collections.keySeq().toArray();
|
const allCollections = searchCollections || state.collections.keySeq().toArray();
|
||||||
const collections = allCollections.filter(collection =>
|
const collections = allCollections.filter(collection =>
|
||||||
selectIntegration(state, collection as string, 'search'),
|
selectIntegration(state, collection as string, 'search'),
|
||||||
);
|
);
|
||||||
@ -139,12 +150,14 @@ export function searchEntries(searchTerm: string, page = 0) {
|
|||||||
if (
|
if (
|
||||||
search.get('isFetching') === true &&
|
search.get('isFetching') === true &&
|
||||||
search.get('term') === searchTerm &&
|
search.get('term') === searchTerm &&
|
||||||
|
search.get('collections') !== null &&
|
||||||
|
List(allCollections).equals(search.get('collections') as List<string>) &&
|
||||||
// if an integration doesn't exist, 'page' is not used
|
// if an integration doesn't exist, 'page' is not used
|
||||||
(search.get('page') === page || !integration)
|
(search.get('page') === page || !integration)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(searchingEntries(searchTerm, page));
|
dispatch(searchingEntries(searchTerm, allCollections as string[], page));
|
||||||
|
|
||||||
const searchPromise = integration
|
const searchPromise = integration
|
||||||
? getIntegrationProvider(state.integrations, backend.getToken, integration).search(
|
? getIntegrationProvider(state.integrations, backend.getToken, integration).search(
|
||||||
@ -152,11 +165,24 @@ export function searchEntries(searchTerm: string, page = 0) {
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
page,
|
page,
|
||||||
)
|
)
|
||||||
: backend.search(state.collections.valueSeq().toArray(), searchTerm);
|
: backend.search(
|
||||||
|
state.collections
|
||||||
|
.filter((_, key: string) => allCollections.indexOf(key) !== -1)
|
||||||
|
.valueSeq()
|
||||||
|
.toArray(),
|
||||||
|
searchTerm,
|
||||||
|
);
|
||||||
|
|
||||||
return searchPromise.then(
|
return searchPromise.then(
|
||||||
(response: Response) =>
|
(response: Response) =>
|
||||||
dispatch(searchSuccess(searchTerm, response.entries, response.pagination)),
|
dispatch(
|
||||||
|
searchSuccess(
|
||||||
|
searchTerm,
|
||||||
|
allCollections as string[],
|
||||||
|
response.entries,
|
||||||
|
response.pagination,
|
||||||
|
),
|
||||||
|
),
|
||||||
(error: Error) => dispatch(searchFailure(searchTerm, error)),
|
(error: Error) => dispatch(searchFailure(searchTerm, error)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -200,6 +200,12 @@ class App extends React.Component {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Redirect exact from="/" to={defaultPath} />
|
<Redirect exact from="/" to={defaultPath} />
|
||||||
<Redirect exact from="/search/" to={defaultPath} />
|
<Redirect exact from="/search/" to={defaultPath} />
|
||||||
|
<RouteInCollection
|
||||||
|
exact
|
||||||
|
collections={collections}
|
||||||
|
path="/collections/:name/search/"
|
||||||
|
render={({ match }) => <Redirect to={`/collections/${match.params.name}`} />}
|
||||||
|
/>
|
||||||
<Redirect
|
<Redirect
|
||||||
// This happens on Identity + Invite Only + External Provider email not matching
|
// This happens on Identity + Invite Only + External Provider email not matching
|
||||||
// the registered user
|
// the registered user
|
||||||
@ -223,6 +229,11 @@ class App extends React.Component {
|
|||||||
collections={collections}
|
collections={collections}
|
||||||
render={props => <Editor {...props} />}
|
render={props => <Editor {...props} />}
|
||||||
/>
|
/>
|
||||||
|
<RouteInCollection
|
||||||
|
path="/collections/:name/search/:searchTerm"
|
||||||
|
collections={collections}
|
||||||
|
render={props => <Collection {...props} isSearchResults isSingleSearchResult />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/search/:searchTerm"
|
path="/search/:searchTerm"
|
||||||
render={props => <Collection {...props} isSearchResults />}
|
render={props => <Collection {...props} isSearchResults />}
|
||||||
|
@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
import { lengths } from 'netlify-cms-ui-default';
|
import { lengths, components } from 'netlify-cms-ui-default';
|
||||||
import { getNewEntryUrl } from 'Lib/urlHelper';
|
import { getNewEntryUrl } from 'Lib/urlHelper';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import CollectionTop from './CollectionTop';
|
import CollectionTop from './CollectionTop';
|
||||||
@ -24,11 +24,21 @@ const CollectionMain = styled.main`
|
|||||||
padding-left: 280px;
|
padding-left: 280px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const SearchResultContainer = styled.div`
|
||||||
|
${components.cardTop};
|
||||||
|
margin-bottom: 22px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SearchResultHeading = styled.h1`
|
||||||
|
${components.cardTopHeading};
|
||||||
|
`;
|
||||||
|
|
||||||
class Collection extends React.Component {
|
class Collection extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
collectionName: PropTypes.string,
|
collectionName: PropTypes.string,
|
||||||
isSearchResults: PropTypes.bool,
|
isSearchResults: PropTypes.bool,
|
||||||
|
isSingleSearchResult: PropTypes.bool,
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
sortableFields: PropTypes.array,
|
sortableFields: PropTypes.array,
|
||||||
@ -46,8 +56,13 @@ class Collection extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderEntriesSearch = () => {
|
renderEntriesSearch = () => {
|
||||||
const { searchTerm, collections } = this.props;
|
const { searchTerm, collections, collection, isSingleSearchResult } = this.props;
|
||||||
return <EntriesSearch collections={collections} searchTerm={searchTerm} />;
|
return (
|
||||||
|
<EntriesSearch
|
||||||
|
collections={isSingleSearchResult ? collections.filter(c => c === collection) : collections}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeViewStyle = viewStyle => {
|
handleChangeViewStyle = viewStyle => {
|
||||||
@ -62,17 +77,33 @@ class Collection extends React.Component {
|
|||||||
collections,
|
collections,
|
||||||
collectionName,
|
collectionName,
|
||||||
isSearchResults,
|
isSearchResults,
|
||||||
|
isSingleSearchResult,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
sortableFields,
|
sortableFields,
|
||||||
onSortClick,
|
onSortClick,
|
||||||
sort,
|
sort,
|
||||||
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const newEntryUrl = collection.get('create') ? getNewEntryUrl(collectionName) : '';
|
const newEntryUrl = collection.get('create') ? getNewEntryUrl(collectionName) : '';
|
||||||
|
|
||||||
|
const searchResultKey =
|
||||||
|
'collection.collectionTop.searchResults' + (isSingleSearchResult ? 'InCollection' : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionContainer>
|
<CollectionContainer>
|
||||||
<Sidebar collections={collections} searchTerm={searchTerm} />
|
<Sidebar
|
||||||
|
collections={collections}
|
||||||
|
collection={(!isSearchResults || isSingleSearchResult) && collection}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
/>
|
||||||
<CollectionMain>
|
<CollectionMain>
|
||||||
{isSearchResults ? null : (
|
{isSearchResults ? (
|
||||||
|
<SearchResultContainer>
|
||||||
|
<SearchResultHeading>
|
||||||
|
{t(searchResultKey, { searchTerm, collection: collection.get('label') })}
|
||||||
|
</SearchResultHeading>
|
||||||
|
</SearchResultContainer>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<CollectionTop collection={collection} newEntryUrl={newEntryUrl} />
|
<CollectionTop collection={collection} newEntryUrl={newEntryUrl} />
|
||||||
<CollectionControls
|
<CollectionControls
|
||||||
|
@ -0,0 +1,238 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { colorsRaw, colors, Icon, lengths, zIndex } from 'netlify-cms-ui-default';
|
||||||
|
import { translate } from 'react-polyglot';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
const SearchContainer = styled.div`
|
||||||
|
margin: 0 12px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
${Icon} {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 6px;
|
||||||
|
z-index: ${zIndex.zIndex2};
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InputContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SearchInput = styled.input`
|
||||||
|
background-color: #eff0f4;
|
||||||
|
border-radius: ${lengths.borderRadius};
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px 6px 10px 32px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: ${zIndex.zIndex1};
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 0 0 2px ${colorsRaw.blue};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SuggestionsContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Suggestions = styled.ul`
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: ${lengths.borderRadius};
|
||||||
|
border: 1px solid ${colors.textFieldBorder};
|
||||||
|
z-index: ${zIndex.zIndex1};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SuggestionHeader = styled.li`
|
||||||
|
padding: 0px 6px 6px 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${colors.text};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SuggestionItem = styled.li(
|
||||||
|
({ isActive }) => `
|
||||||
|
color: ${isActive ? colors.active : colorsRaw.grayDark};
|
||||||
|
background-color: ${isActive ? colors.activeBackground : 'inherit'};
|
||||||
|
padding: 6px 6px 6px 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${colors.active};
|
||||||
|
background-color: ${colors.activeBackground};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const SuggestionDivider = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
class CollectionSearch extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
|
collection: ImmutablePropTypes.map,
|
||||||
|
searchTerm: PropTypes.string.isRequired,
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
query: this.props.searchTerm,
|
||||||
|
suggestionsVisible: false,
|
||||||
|
// default to the currently selected
|
||||||
|
selectedCollectionIdx: this.getSelectedSelectionBasedOnProps(),
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.collection !== this.props.collection) {
|
||||||
|
const selectedCollectionIdx = this.getSelectedSelectionBasedOnProps();
|
||||||
|
this.setState({ selectedCollectionIdx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedSelectionBasedOnProps() {
|
||||||
|
const { collection, collections } = this.props;
|
||||||
|
return collection ? collections.keySeq().indexOf(collection.get('name')) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSuggestions(visible) {
|
||||||
|
this.setState({ suggestionsVisible: visible });
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNextSuggestion() {
|
||||||
|
const { collections } = this.props;
|
||||||
|
const { selectedCollectionIdx } = this.state;
|
||||||
|
this.setState({
|
||||||
|
selectedCollectionIdx: Math.min(selectedCollectionIdx + 1, collections.size - 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPreviousSuggestion() {
|
||||||
|
const { selectedCollectionIdx } = this.state;
|
||||||
|
this.setState({
|
||||||
|
selectedCollectionIdx: Math.max(selectedCollectionIdx - 1, -1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSelectedSuggestion() {
|
||||||
|
this.setState({
|
||||||
|
selectedCollectionIdx: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submitSearch = () => {
|
||||||
|
const { onSubmit, collections } = this.props;
|
||||||
|
const { selectedCollectionIdx, query } = this.state;
|
||||||
|
|
||||||
|
this.toggleSuggestions(false);
|
||||||
|
if (selectedCollectionIdx !== -1) {
|
||||||
|
onSubmit(query, collections.toIndexedSeq().getIn([selectedCollectionIdx, 'name']));
|
||||||
|
} else {
|
||||||
|
onSubmit(query);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeyDown = event => {
|
||||||
|
const { suggestionsVisible } = this.state;
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
this.submitSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestionsVisible) {
|
||||||
|
// allow closing of suggestions with escape key
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.toggleSuggestions(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
this.selectNextSuggestion();
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
this.selectPreviousSuggestion();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleQueryChange = query => {
|
||||||
|
this.setState({ query });
|
||||||
|
this.toggleSuggestions(query !== '');
|
||||||
|
if (query === '') {
|
||||||
|
this.resetSelectedSuggestion();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSuggestionClick = (event, idx) => {
|
||||||
|
this.setState({ selectedCollectionIdx: idx }, this.submitSearch);
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { collections, t } = this.props;
|
||||||
|
const { suggestionsVisible, selectedCollectionIdx, query } = this.state;
|
||||||
|
return (
|
||||||
|
<SearchContainer
|
||||||
|
onBlur={() => this.toggleSuggestions(false)}
|
||||||
|
onFocus={() => this.toggleSuggestions(query !== '')}
|
||||||
|
>
|
||||||
|
<InputContainer>
|
||||||
|
<Icon type="search" />
|
||||||
|
<SearchInput
|
||||||
|
onChange={e => this.handleQueryChange(e.target.value)}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onClick={() => this.toggleSuggestions(true)}
|
||||||
|
placeholder={t('collection.sidebar.searchAll')}
|
||||||
|
value={query}
|
||||||
|
/>
|
||||||
|
</InputContainer>
|
||||||
|
{suggestionsVisible && (
|
||||||
|
<SuggestionsContainer>
|
||||||
|
<Suggestions>
|
||||||
|
<SuggestionHeader>{t('collection.sidebar.searchIn')}</SuggestionHeader>
|
||||||
|
<SuggestionItem
|
||||||
|
isActive={selectedCollectionIdx === -1}
|
||||||
|
onClick={e => this.handleSuggestionClick(e, -1)}
|
||||||
|
onMouseDown={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t('collection.sidebar.allCollections')}
|
||||||
|
</SuggestionItem>
|
||||||
|
<SuggestionDivider />
|
||||||
|
{collections.toIndexedSeq().map((collection, idx) => (
|
||||||
|
<SuggestionItem
|
||||||
|
key={idx}
|
||||||
|
isActive={idx === selectedCollectionIdx}
|
||||||
|
onClick={e => this.handleSuggestionClick(e, idx)}
|
||||||
|
onMouseDown={e => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{collection.get('label')}
|
||||||
|
</SuggestionItem>
|
||||||
|
))}
|
||||||
|
</Suggestions>
|
||||||
|
</SuggestionsContainer>
|
||||||
|
)}
|
||||||
|
</SearchContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate()(CollectionSearch);
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
import { Cursor } from 'netlify-cms-lib-util';
|
import { Cursor } from 'netlify-cms-lib-util';
|
||||||
import { selectSearchedEntries } from 'Reducers';
|
import { selectSearchedEntries } from 'Reducers';
|
||||||
import {
|
import {
|
||||||
@ -17,19 +18,25 @@ class EntriesSearch extends React.Component {
|
|||||||
clearSearch: PropTypes.func.isRequired,
|
clearSearch: PropTypes.func.isRequired,
|
||||||
searchTerm: PropTypes.string.isRequired,
|
searchTerm: PropTypes.string.isRequired,
|
||||||
collections: ImmutablePropTypes.seq,
|
collections: ImmutablePropTypes.seq,
|
||||||
|
collectionNames: PropTypes.array,
|
||||||
entries: ImmutablePropTypes.list,
|
entries: ImmutablePropTypes.list,
|
||||||
page: PropTypes.number,
|
page: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { searchTerm, searchEntries } = this.props;
|
const { searchTerm, searchEntries, collectionNames } = this.props;
|
||||||
searchEntries(searchTerm);
|
searchEntries(searchTerm, collectionNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.searchTerm === this.props.searchTerm) return;
|
const { searchTerm, collectionNames } = this.props;
|
||||||
|
|
||||||
|
// check if the search parameters are the same
|
||||||
|
if (prevProps.searchTerm === searchTerm && isEqual(prevProps.collectionNames, collectionNames))
|
||||||
|
return;
|
||||||
|
|
||||||
const { searchEntries } = prevProps;
|
const { searchEntries } = prevProps;
|
||||||
searchEntries(this.props.searchTerm);
|
searchEntries(searchTerm, collectionNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -44,10 +51,10 @@ class EntriesSearch extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleCursorActions = action => {
|
handleCursorActions = action => {
|
||||||
const { page, searchTerm, searchEntries } = this.props;
|
const { page, searchTerm, searchEntries, collectionNames } = this.props;
|
||||||
if (action === 'append_next') {
|
if (action === 'append_next') {
|
||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
searchEntries(searchTerm, nextPage);
|
searchEntries(searchTerm, collectionNames, nextPage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,11 +75,11 @@ class EntriesSearch extends React.Component {
|
|||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
const { searchTerm } = ownProps;
|
const { searchTerm } = ownProps;
|
||||||
const collections = ownProps.collections.toIndexedSeq();
|
const collections = ownProps.collections.toIndexedSeq();
|
||||||
|
const collectionNames = ownProps.collections.keySeq().toArray();
|
||||||
const isFetching = state.search.get('isFetching');
|
const isFetching = state.search.get('isFetching');
|
||||||
const page = state.search.get('page');
|
const page = state.search.get('page');
|
||||||
const entries = selectSearchedEntries(state);
|
const entries = selectSearchedEntries(state, collectionNames);
|
||||||
|
return { isFetching, page, collections, collectionNames, entries, searchTerm };
|
||||||
return { isFetching, page, collections, entries, searchTerm };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -54,10 +54,11 @@ export default class EntryListing extends React.Component {
|
|||||||
|
|
||||||
renderCardsForMultipleCollections = () => {
|
renderCardsForMultipleCollections = () => {
|
||||||
const { collections, entries } = this.props;
|
const { collections, entries } = this.props;
|
||||||
|
const isSingleCollectionInList = collections.size === 1;
|
||||||
return entries.map((entry, idx) => {
|
return entries.map((entry, idx) => {
|
||||||
const collectionName = entry.get('collection');
|
const collectionName = entry.get('collection');
|
||||||
const collection = collections.find(coll => coll.get('name') === collectionName);
|
const collection = collections.find(coll => coll.get('name') === collectionName);
|
||||||
const collectionLabel = collection.get('label');
|
const collectionLabel = !isSingleCollectionInList && collection.get('label');
|
||||||
const inferedFields = this.inferFields(collection);
|
const inferedFields = this.inferFields(collection);
|
||||||
const entryCardProps = { collection, entry, inferedFields, collectionLabel };
|
const entryCardProps = { collection, entry, inferedFields, collectionLabel };
|
||||||
return <EntryCard {...entryCardProps} key={idx} />;
|
return <EntryCard {...entryCardProps} key={idx} />;
|
||||||
|
@ -5,8 +5,9 @@ import styled from '@emotion/styled';
|
|||||||
import { css } from '@emotion/core';
|
import { css } from '@emotion/core';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { Icon, components, colors, colorsRaw, lengths, zIndex } from 'netlify-cms-ui-default';
|
import { Icon, components, colors } from 'netlify-cms-ui-default';
|
||||||
import { searchCollections } from 'Actions/collections';
|
import { searchCollections } from 'Actions/collections';
|
||||||
|
import CollectionSearch from './CollectionSearch';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
sidebarNavLinkActive: css`
|
sidebarNavLinkActive: css`
|
||||||
@ -22,7 +23,8 @@ const SidebarContainer = styled.aside`
|
|||||||
padding: 8px 0 12px;
|
padding: 8px 0 12px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
max-height: calc(100vh - 112px);
|
max-height: calc(100vh - 112px);
|
||||||
overflow: auto;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SidebarHeading = styled.h2`
|
const SidebarHeading = styled.h2`
|
||||||
@ -33,42 +35,10 @@ const SidebarHeading = styled.h2`
|
|||||||
color: ${colors.textLead};
|
color: ${colors.textLead};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SearchContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 12px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
${Icon} {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 6px;
|
|
||||||
z-index: ${zIndex.zIndex2};
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SearchInput = styled.input`
|
|
||||||
background-color: #eff0f4;
|
|
||||||
border-radius: ${lengths.borderRadius};
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 10px 6px 10px 32px;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: ${zIndex.zIndex1};
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: inset 0 0 0 2px ${colorsRaw.blue};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SidebarNavList = styled.ul`
|
const SidebarNavList = styled.ul`
|
||||||
margin: 16px 0 0;
|
margin: 16px 0 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
overflow: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SidebarNavLink = styled(NavLink)`
|
const SidebarNavLink = styled(NavLink)`
|
||||||
@ -78,6 +48,7 @@ const SidebarNavLink = styled(NavLink)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-left: 2px solid #fff;
|
border-left: 2px solid #fff;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
${Icon} {
|
${Icon} {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
@ -96,6 +67,7 @@ const SidebarNavLink = styled(NavLink)`
|
|||||||
class Sidebar extends React.Component {
|
class Sidebar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
|
collection: ImmutablePropTypes.map,
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@ -104,8 +76,6 @@ class Sidebar extends React.Component {
|
|||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
state = { query: this.props.searchTerm };
|
|
||||||
|
|
||||||
renderLink = collection => {
|
renderLink = collection => {
|
||||||
const collectionName = collection.get('name');
|
const collectionName = collection.get('name');
|
||||||
return (
|
return (
|
||||||
@ -119,21 +89,16 @@ class Sidebar extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { collections, t } = this.props;
|
const { collections, collection, searchTerm, t } = this.props;
|
||||||
const { query } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<SidebarHeading>{t('collection.sidebar.collections')}</SidebarHeading>
|
<SidebarHeading>{t('collection.sidebar.collections')}</SidebarHeading>
|
||||||
<SearchContainer>
|
<CollectionSearch
|
||||||
<Icon type="search" size="small" />
|
searchTerm={searchTerm}
|
||||||
<SearchInput
|
collections={collections}
|
||||||
onChange={e => this.setState({ query: e.target.value })}
|
collection={collection}
|
||||||
onKeyDown={e => e.key === 'Enter' && searchCollections(query)}
|
onSubmit={(query, collection) => searchCollections(query, collection)}
|
||||||
placeholder={t('collection.sidebar.searchAll')}
|
/>
|
||||||
value={query}
|
|
||||||
/>
|
|
||||||
</SearchContainer>
|
|
||||||
<SidebarNavList>
|
<SidebarNavList>
|
||||||
{collections
|
{collections
|
||||||
.toList()
|
.toList()
|
||||||
|
@ -44,13 +44,14 @@ export const selectEntries = (state: State, collection: string) =>
|
|||||||
export const selectPublishedSlugs = (state: State, collection: string) =>
|
export const selectPublishedSlugs = (state: State, collection: string) =>
|
||||||
fromEntries.selectPublishedSlugs(state.entries, collection);
|
fromEntries.selectPublishedSlugs(state.entries, collection);
|
||||||
|
|
||||||
export const selectSearchedEntries = (state: State) => {
|
export const selectSearchedEntries = (state: State, availableCollections: string[]) => {
|
||||||
const searchItems = state.search.get('entryIds');
|
const searchItems = state.search.get('entryIds');
|
||||||
|
// only return search results for actually available collections
|
||||||
return (
|
return (
|
||||||
searchItems &&
|
searchItems &&
|
||||||
searchItems.map(({ collection, slug }) =>
|
searchItems
|
||||||
fromEntries.selectEntry(state.entries, collection, slug),
|
.filter(({ collection }) => availableCollections.indexOf(collection) !== -1)
|
||||||
)
|
.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,10 +12,12 @@ let loadedEntries;
|
|||||||
let response;
|
let response;
|
||||||
let page;
|
let page;
|
||||||
let searchTerm;
|
let searchTerm;
|
||||||
|
let searchCollections;
|
||||||
|
|
||||||
const defaultState = Map({
|
const defaultState = Map({
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
term: null,
|
term: null,
|
||||||
|
collections: null,
|
||||||
page: 0,
|
page: 0,
|
||||||
entryIds: List([]),
|
entryIds: List([]),
|
||||||
queryHits: Map({}),
|
queryHits: Map({}),
|
||||||
@ -31,6 +33,7 @@ const entries = (state = defaultState, action) => {
|
|||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('isFetching', true);
|
map.set('isFetching', true);
|
||||||
map.set('term', action.payload.searchTerm);
|
map.set('term', action.payload.searchTerm);
|
||||||
|
map.set('collections', List(action.payload.searchCollections));
|
||||||
map.set('page', action.payload.page);
|
map.set('page', action.payload.page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -40,6 +43,7 @@ const entries = (state = defaultState, action) => {
|
|||||||
loadedEntries = action.payload.entries;
|
loadedEntries = action.payload.entries;
|
||||||
page = action.payload.page;
|
page = action.payload.page;
|
||||||
searchTerm = action.payload.searchTerm;
|
searchTerm = action.payload.searchTerm;
|
||||||
|
searchCollections = action.payload.searchCollections;
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
const entryIds = List(
|
const entryIds = List(
|
||||||
loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })),
|
loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })),
|
||||||
@ -48,6 +52,7 @@ const entries = (state = defaultState, action) => {
|
|||||||
map.set('fetchID', null);
|
map.set('fetchID', null);
|
||||||
map.set('page', page);
|
map.set('page', page);
|
||||||
map.set('term', searchTerm);
|
map.set('term', searchTerm);
|
||||||
|
map.set('collections', List(searchCollections));
|
||||||
map.set(
|
map.set(
|
||||||
'entryIds',
|
'entryIds',
|
||||||
!page || isNaN(page) || page === 0
|
!page || isNaN(page) || page === 0
|
||||||
|
@ -222,6 +222,7 @@ export type Search = StaticallyTypedRecord<{
|
|||||||
entryIds?: SearchItem[];
|
entryIds?: SearchItem[];
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
term: string | null;
|
term: string | null;
|
||||||
|
collections: List<string> | null;
|
||||||
page: number;
|
page: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -34,11 +34,18 @@ const de = {
|
|||||||
collection: {
|
collection: {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
collections: 'Inhaltstypen',
|
collections: 'Inhaltstypen',
|
||||||
|
allCollections: 'Allen Inhaltstypen',
|
||||||
searchAll: 'Alles durchsuchen',
|
searchAll: 'Alles durchsuchen',
|
||||||
|
searchIn: 'Suchen in',
|
||||||
},
|
},
|
||||||
collectionTop: {
|
collectionTop: {
|
||||||
|
sortBy: 'Sortieren nach',
|
||||||
viewAs: 'Anzeigen als',
|
viewAs: 'Anzeigen als',
|
||||||
newButton: 'Neue(r) %{collectionLabel}',
|
newButton: 'Neue(r) %{collectionLabel}',
|
||||||
|
ascending: 'Aufsteigend',
|
||||||
|
descending: 'Absteigend',
|
||||||
|
searchResults: 'Suchergebnisse für "%{searchTerm}"',
|
||||||
|
searchResultsInCollection: 'Suchergebnisse für "%{searchTerm}" in %{collection}',
|
||||||
},
|
},
|
||||||
entries: {
|
entries: {
|
||||||
loadingEntries: 'Beiträge laden',
|
loadingEntries: 'Beiträge laden',
|
||||||
|
@ -34,7 +34,9 @@ const en = {
|
|||||||
collection: {
|
collection: {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
collections: 'Collections',
|
collections: 'Collections',
|
||||||
|
allCollections: 'All Collections',
|
||||||
searchAll: 'Search all',
|
searchAll: 'Search all',
|
||||||
|
searchIn: 'Search in',
|
||||||
},
|
},
|
||||||
collectionTop: {
|
collectionTop: {
|
||||||
sortBy: 'Sort by',
|
sortBy: 'Sort by',
|
||||||
@ -42,6 +44,8 @@ const en = {
|
|||||||
newButton: 'New %{collectionLabel}',
|
newButton: 'New %{collectionLabel}',
|
||||||
ascending: 'Ascending',
|
ascending: 'Ascending',
|
||||||
descending: 'Descending',
|
descending: 'Descending',
|
||||||
|
searchResults: 'Search Results for "%{searchTerm}"',
|
||||||
|
searchResultsInCollection: 'Search Results for "%{searchTerm}" in %{collection}',
|
||||||
},
|
},
|
||||||
entries: {
|
entries: {
|
||||||
loadingEntries: 'Loading Entries...',
|
loadingEntries: 'Loading Entries...',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user