refactor(core): refactor search actions and reducer
Convert to TS Proper search action type Replace immutable with immer General cleanup
This commit is contained in:
parent
3211f94f4a
commit
e32ffdf587
@ -22,7 +22,7 @@ describe('search', () => {
|
|||||||
it('should search entries in all collections 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: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
selectIntegration.mockReturnValue('search_integration');
|
selectIntegration.mockReturnValue('search_integration');
|
||||||
@ -46,8 +46,6 @@ describe('search', () => {
|
|||||||
expect(actions[1]).toEqual({
|
expect(actions[1]).toEqual({
|
||||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
|
||||||
searchCollections: ['posts', 'pages'],
|
|
||||||
entries: response.entries,
|
entries: response.entries,
|
||||||
page: response.pagination,
|
page: response.pagination,
|
||||||
},
|
},
|
||||||
@ -60,7 +58,7 @@ describe('search', () => {
|
|||||||
it('should search entries in a subset of collections using integration', async () => {
|
it('should search entries in a subset of 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: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
selectIntegration.mockReturnValue('search_integration');
|
selectIntegration.mockReturnValue('search_integration');
|
||||||
@ -84,8 +82,6 @@ describe('search', () => {
|
|||||||
expect(actions[1]).toEqual({
|
expect(actions[1]).toEqual({
|
||||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
|
||||||
searchCollections: ['pages'],
|
|
||||||
entries: response.entries,
|
entries: response.entries,
|
||||||
page: response.pagination,
|
page: response.pagination,
|
||||||
},
|
},
|
||||||
@ -98,7 +94,7 @@ describe('search', () => {
|
|||||||
it('should search entries in all collections using backend', async () => {
|
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: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||||
@ -121,8 +117,6 @@ describe('search', () => {
|
|||||||
expect(actions[1]).toEqual({
|
expect(actions[1]).toEqual({
|
||||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
|
||||||
searchCollections: ['posts', 'pages'],
|
|
||||||
entries: response.entries,
|
entries: response.entries,
|
||||||
page: response.pagination,
|
page: response.pagination,
|
||||||
},
|
},
|
||||||
@ -138,7 +132,7 @@ describe('search', () => {
|
|||||||
it('should search entries in a subset of collections using backend', 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({}),
|
search: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
const response = { entries: [{ name: '1' }, { name: '' }], pagination: 1 };
|
||||||
@ -161,8 +155,6 @@ describe('search', () => {
|
|||||||
expect(actions[1]).toEqual({
|
expect(actions[1]).toEqual({
|
||||||
type: 'SEARCH_ENTRIES_SUCCESS',
|
type: 'SEARCH_ENTRIES_SUCCESS',
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm: 'find me',
|
|
||||||
searchCollections: ['pages'],
|
|
||||||
entries: response.entries,
|
entries: response.entries,
|
||||||
page: response.pagination,
|
page: response.pagination,
|
||||||
},
|
},
|
||||||
@ -175,7 +167,7 @@ describe('search', () => {
|
|||||||
it('should ignore identical search in all collections', async () => {
|
it('should ignore identical search in all collections', 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', collections: ['posts', 'pages'] }),
|
search: { isFetching: true, term: 'find me', collections: ['posts', 'pages'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.dispatch(searchEntries('find me'));
|
await store.dispatch(searchEntries('find me'));
|
||||||
@ -187,7 +179,7 @@ describe('search', () => {
|
|||||||
it('should ignore identical search in a subset of collections', async () => {
|
it('should ignore identical search in a subset of collections', 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', collections: ['pages'] }),
|
search: { isFetching: true, term: 'find me', collections: ['pages'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.dispatch(searchEntries('find me', ['pages']));
|
await store.dispatch(searchEntries('find me', ['pages']));
|
||||||
@ -199,7 +191,7 @@ describe('search', () => {
|
|||||||
it('should not ignore same search term in different search collections', async () => {
|
it('should not ignore same search term in different search collections', 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', collections: ['pages'] }),
|
search: { isFetching: true, term: 'find me', collections: ['pages'] },
|
||||||
});
|
});
|
||||||
const backend = { search: jest.fn().mockResolvedValue({}) };
|
const backend = { search: jest.fn().mockResolvedValue({}) };
|
||||||
currentBackend.mockReturnValue(backend);
|
currentBackend.mockReturnValue(backend);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
import { State } from '../types/redux';
|
import { State } from '../types/redux';
|
||||||
import { currentBackend } from '../backend';
|
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
|
||||||
@ -14,9 +14,9 @@ export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST';
|
|||||||
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
|
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
|
||||||
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
|
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
|
||||||
|
|
||||||
export const QUERY_REQUEST = 'INIT_QUERY';
|
export const QUERY_REQUEST = 'QUERY_REQUEST';
|
||||||
export const QUERY_SUCCESS = 'QUERY_OK';
|
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
|
||||||
export const QUERY_FAILURE = 'QUERY_ERROR';
|
export const QUERY_FAILURE = 'QUERY_FAILURE';
|
||||||
|
|
||||||
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||||
|
|
||||||
@ -28,51 +28,33 @@ export function searchingEntries(searchTerm: string, searchCollections: string[]
|
|||||||
return {
|
return {
|
||||||
type: SEARCH_ENTRIES_REQUEST,
|
type: SEARCH_ENTRIES_REQUEST,
|
||||||
payload: { searchTerm, searchCollections, page },
|
payload: { searchTerm, searchCollections, page },
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchSuccess(
|
export function searchSuccess(entries: EntryValue[], page: number) {
|
||||||
searchTerm: string,
|
|
||||||
searchCollections: string[],
|
|
||||||
entries: EntryValue[],
|
|
||||||
page: number,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: SEARCH_ENTRIES_SUCCESS,
|
type: SEARCH_ENTRIES_SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
searchTerm,
|
|
||||||
searchCollections,
|
|
||||||
entries,
|
entries,
|
||||||
page,
|
page,
|
||||||
},
|
},
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchFailure(searchTerm: string, error: Error) {
|
export function searchFailure(error: Error) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_ENTRIES_FAILURE,
|
type: SEARCH_ENTRIES_FAILURE,
|
||||||
payload: {
|
payload: { error },
|
||||||
searchTerm,
|
} as const;
|
||||||
error,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function querying(
|
export function querying(searchTerm: string) {
|
||||||
namespace: string,
|
|
||||||
collection: string,
|
|
||||||
searchFields: string[],
|
|
||||||
searchTerm: string,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: QUERY_REQUEST,
|
type: QUERY_REQUEST,
|
||||||
payload: {
|
payload: {
|
||||||
namespace,
|
|
||||||
collection,
|
|
||||||
searchFields,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
},
|
},
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResponse = {
|
type SearchResponse = {
|
||||||
@ -85,42 +67,21 @@ type QueryResponse = {
|
|||||||
query: string;
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function querySuccess(
|
export function querySuccess(namespace: string, hits: EntryValue[]) {
|
||||||
namespace: string,
|
|
||||||
collection: string,
|
|
||||||
searchFields: string[],
|
|
||||||
searchTerm: string,
|
|
||||||
response: QueryResponse,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: QUERY_SUCCESS,
|
type: QUERY_SUCCESS,
|
||||||
payload: {
|
payload: {
|
||||||
namespace,
|
namespace,
|
||||||
collection,
|
hits,
|
||||||
searchFields,
|
|
||||||
searchTerm,
|
|
||||||
response,
|
|
||||||
},
|
},
|
||||||
};
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryFailure(
|
export function queryFailure(error: Error) {
|
||||||
namespace: string,
|
|
||||||
collection: string,
|
|
||||||
searchFields: string[],
|
|
||||||
searchTerm: string,
|
|
||||||
error: Error,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: QUERY_FAILURE,
|
type: QUERY_FAILURE,
|
||||||
payload: {
|
payload: { error },
|
||||||
namespace,
|
} as const;
|
||||||
collection,
|
|
||||||
searchFields,
|
|
||||||
searchTerm,
|
|
||||||
error,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -128,7 +89,7 @@ export function queryFailure(
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function clearSearch() {
|
export function clearSearch() {
|
||||||
return { type: SEARCH_CLEAR };
|
return { type: SEARCH_CLEAR } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -136,33 +97,29 @@ export function clearSearch() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// SearchEntries will search for complete entries in all collections.
|
// SearchEntries will search for complete entries in all collections.
|
||||||
export function searchEntries(
|
export function searchEntries(searchTerm: string, searchCollections: string[], page = 0) {
|
||||||
searchTerm: string,
|
return async (dispatch: ThunkDispatch<State, undefined, AnyAction>, getState: () => State) => {
|
||||||
searchCollections: string[] | null = null,
|
|
||||||
page = 0,
|
|
||||||
) {
|
|
||||||
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 = searchCollections || 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, 'search'),
|
||||||
);
|
);
|
||||||
const integration = selectIntegration(state, collections[0] as string, 'search');
|
const integration = selectIntegration(state, collections[0], 'search');
|
||||||
|
|
||||||
// avoid duplicate searches
|
// avoid duplicate searches
|
||||||
if (
|
if (
|
||||||
search.get('isFetching') === true &&
|
search.isFetching &&
|
||||||
search.get('term') === searchTerm &&
|
search.term === searchTerm &&
|
||||||
search.get('collections') !== null &&
|
isEqual(allCollections, search.collections) &&
|
||||||
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.page === page || !integration)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(searchingEntries(searchTerm, allCollections as string[], page));
|
|
||||||
|
dispatch(searchingEntries(searchTerm, allCollections, page));
|
||||||
|
|
||||||
const searchPromise = integration
|
const searchPromise = integration
|
||||||
? getIntegrationProvider(state.integrations, backend.getToken, integration).search(
|
? getIntegrationProvider(state.integrations, backend.getToken, integration).search(
|
||||||
@ -178,18 +135,12 @@ export function searchEntries(
|
|||||||
searchTerm,
|
searchTerm,
|
||||||
);
|
);
|
||||||
|
|
||||||
return searchPromise.then(
|
try {
|
||||||
(response: SearchResponse) =>
|
const response: SearchResponse = await searchPromise;
|
||||||
dispatch(
|
return dispatch(searchSuccess(response.entries, response.pagination));
|
||||||
searchSuccess(
|
} catch (error) {
|
||||||
searchTerm,
|
return dispatch(searchFailure(error));
|
||||||
allCollections as string[],
|
}
|
||||||
response.entries,
|
|
||||||
response.pagination,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(error: Error) => dispatch(searchFailure(searchTerm, error)),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +155,7 @@ export function query(
|
|||||||
limit?: number,
|
limit?: number,
|
||||||
) {
|
) {
|
||||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||||
dispatch(querying(namespace, collectionName, searchFields, searchTerm));
|
dispatch(querying(searchTerm));
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
@ -223,9 +174,19 @@ export function query(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response: QueryResponse = await queryPromise;
|
const response: QueryResponse = await queryPromise;
|
||||||
return dispatch(querySuccess(namespace, collectionName, searchFields, searchTerm, response));
|
return dispatch(querySuccess(namespace, response.hits));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return dispatch(queryFailure(namespace, collectionName, searchFields, searchTerm, error));
|
return dispatch(queryFailure(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SearchAction = ReturnType<
|
||||||
|
| typeof searchingEntries
|
||||||
|
| typeof searchSuccess
|
||||||
|
| typeof searchFailure
|
||||||
|
| typeof querying
|
||||||
|
| typeof querySuccess
|
||||||
|
| typeof queryFailure
|
||||||
|
| typeof clearSearch
|
||||||
|
>;
|
||||||
|
@ -76,8 +76,8 @@ 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 collectionNames = ownProps.collections.keySeq().toArray();
|
||||||
const isFetching = state.search.get('isFetching');
|
const isFetching = state.search.isFetching;
|
||||||
const page = state.search.get('page');
|
const page = state.search.page;
|
||||||
const entries = selectSearchedEntries(state, collectionNames);
|
const entries = selectSearchedEntries(state, collectionNames);
|
||||||
return { isFetching, page, collections, collectionNames, entries, searchTerm };
|
return { isFetching, page, collections, collectionNames, entries, searchTerm };
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ class EditorControl extends React.Component {
|
|||||||
processControlRef: PropTypes.func,
|
processControlRef: PropTypes.func,
|
||||||
controlRef: PropTypes.func,
|
controlRef: PropTypes.func,
|
||||||
query: PropTypes.func.isRequired,
|
query: PropTypes.func.isRequired,
|
||||||
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
queryHits: PropTypes.object,
|
||||||
isFetching: PropTypes.bool,
|
isFetching: PropTypes.bool,
|
||||||
clearSearch: PropTypes.func.isRequired,
|
clearSearch: PropTypes.func.isRequired,
|
||||||
clearFieldErrors: PropTypes.func.isRequired,
|
clearFieldErrors: PropTypes.func.isRequired,
|
||||||
@ -311,7 +311,7 @@ class EditorControl extends React.Component {
|
|||||||
editorControl={ConnectedEditorControl}
|
editorControl={ConnectedEditorControl}
|
||||||
query={query}
|
query={query}
|
||||||
loadEntry={loadEntry}
|
loadEntry={loadEntry}
|
||||||
queryHits={queryHits}
|
queryHits={queryHits[this.uniqueFieldId] || []}
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
clearFieldErrors={clearFieldErrors}
|
clearFieldErrors={clearFieldErrors}
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
@ -356,8 +356,8 @@ function mapStateToProps(state) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mediaPaths: state.mediaLibrary.get('controlMedia'),
|
mediaPaths: state.mediaLibrary.get('controlMedia'),
|
||||||
isFetching: state.search.get('isFetching'),
|
isFetching: state.search.isFetching,
|
||||||
queryHits: state.search.get('queryHits'),
|
queryHits: state.search.queryHits,
|
||||||
config: state.config,
|
config: state.config,
|
||||||
entry,
|
entry,
|
||||||
collection,
|
collection,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { List } from 'immutable';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import integrations, * as fromIntegrations from './integrations';
|
import integrations, * as fromIntegrations from './integrations';
|
||||||
@ -50,14 +51,10 @@ export function selectPublishedSlugs(state: State, collection: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function selectSearchedEntries(state: State, availableCollections: string[]) {
|
export function selectSearchedEntries(state: State, availableCollections: string[]) {
|
||||||
const searchItems = state.search.get('entryIds');
|
|
||||||
// only return search results for actually available collections
|
// only return search results for actually available collections
|
||||||
return (
|
return List(state.search.entryIds)
|
||||||
searchItems &&
|
.filter(entryId => availableCollections.indexOf(entryId!.collection) !== -1)
|
||||||
searchItems
|
.map(entryId => fromEntries.selectEntry(state.entries, entryId!.collection, entryId!.slug));
|
||||||
.filter(({ collection }) => availableCollections.indexOf(collection) !== -1)
|
|
||||||
.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectDeployPreview(state: State, collection: string, slug: string) {
|
export function selectDeployPreview(state: State, collection: string, slug: string) {
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
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;
|
|
||||||
let searchCollections;
|
|
||||||
|
|
||||||
const defaultState = Map({
|
|
||||||
isFetching: false,
|
|
||||||
term: null,
|
|
||||||
collections: null,
|
|
||||||
page: 0,
|
|
||||||
entryIds: List([]),
|
|
||||||
queryHits: Map({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function 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);
|
|
||||||
map.set('collections', List(action.payload.searchCollections));
|
|
||||||
map.set('page', action.payload.page);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
|
|
||||||
case SEARCH_ENTRIES_SUCCESS:
|
|
||||||
loadedEntries = action.payload.entries;
|
|
||||||
page = action.payload.page;
|
|
||||||
searchTerm = action.payload.searchTerm;
|
|
||||||
searchCollections = action.payload.searchCollections;
|
|
||||||
return state.withMutations(map => {
|
|
||||||
const entryIds = List(
|
|
||||||
loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })),
|
|
||||||
);
|
|
||||||
map.set('isFetching', false);
|
|
||||||
map.set('fetchID', null);
|
|
||||||
map.set('page', page);
|
|
||||||
map.set('term', searchTerm);
|
|
||||||
map.set('collections', List(searchCollections));
|
|
||||||
map.set(
|
|
||||||
'entryIds',
|
|
||||||
!page || isNaN(page) || 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', action.payload.namespace ? true : false);
|
|
||||||
map.set('fetchID', action.payload.namespace);
|
|
||||||
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('fetchID', null);
|
|
||||||
map.set('term', searchTerm);
|
|
||||||
map.mergeIn(['queryHits'], Map({ [action.payload.namespace]: response.hits }));
|
|
||||||
});
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default entries;
|
|
88
packages/netlify-cms-core/src/reducers/search.ts
Normal file
88
packages/netlify-cms-core/src/reducers/search.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { produce } from 'immer';
|
||||||
|
|
||||||
|
import {
|
||||||
|
QUERY_FAILURE,
|
||||||
|
QUERY_REQUEST,
|
||||||
|
QUERY_SUCCESS,
|
||||||
|
SEARCH_CLEAR,
|
||||||
|
SEARCH_ENTRIES_FAILURE,
|
||||||
|
SEARCH_ENTRIES_REQUEST,
|
||||||
|
SEARCH_ENTRIES_SUCCESS,
|
||||||
|
SearchAction,
|
||||||
|
} from '../actions/search';
|
||||||
|
import { EntryValue } from '../valueObjects/Entry';
|
||||||
|
|
||||||
|
export type Search = {
|
||||||
|
isFetching: boolean;
|
||||||
|
term: string;
|
||||||
|
collections: string[];
|
||||||
|
page: number;
|
||||||
|
entryIds: { collection: string; slug: string }[];
|
||||||
|
queryHits: Record<string, EntryValue[]>;
|
||||||
|
error: Error | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState: Search = {
|
||||||
|
isFetching: false,
|
||||||
|
term: '',
|
||||||
|
collections: [],
|
||||||
|
page: 0,
|
||||||
|
entryIds: [],
|
||||||
|
queryHits: {},
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = produce((state: Search, action: SearchAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SEARCH_CLEAR:
|
||||||
|
return defaultState;
|
||||||
|
|
||||||
|
case SEARCH_ENTRIES_REQUEST: {
|
||||||
|
const { page, searchTerm, searchCollections } = action.payload;
|
||||||
|
state.isFetching = true;
|
||||||
|
state.term = searchTerm;
|
||||||
|
state.collections = searchCollections;
|
||||||
|
state.page = page;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SEARCH_ENTRIES_SUCCESS: {
|
||||||
|
const { entries, page } = action.payload;
|
||||||
|
const entryIds = entries.map(entry => ({ collection: entry.collection, slug: entry.slug }));
|
||||||
|
state.isFetching = false;
|
||||||
|
state.page = page;
|
||||||
|
state.entryIds =
|
||||||
|
!page || isNaN(page) || page === 0 ? entryIds : state.entryIds.concat(entryIds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SEARCH_ENTRIES_FAILURE: {
|
||||||
|
const { error } = action.payload;
|
||||||
|
state.isFetching = false;
|
||||||
|
state.error = error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QUERY_REQUEST: {
|
||||||
|
const { searchTerm } = action.payload;
|
||||||
|
state.isFetching = true;
|
||||||
|
state.term = searchTerm;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QUERY_SUCCESS: {
|
||||||
|
const { namespace, hits } = action.payload;
|
||||||
|
state.isFetching = false;
|
||||||
|
state.queryHits[namespace] = hits;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QUERY_FAILURE: {
|
||||||
|
const { error } = action.payload;
|
||||||
|
state.isFetching = false;
|
||||||
|
state.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, defaultState);
|
||||||
|
|
||||||
|
export default search;
|
@ -7,6 +7,7 @@ import { Auth } from '../reducers/auth';
|
|||||||
import { Status } from '../reducers/status';
|
import { Status } from '../reducers/status';
|
||||||
import { Medias } from '../reducers/medias';
|
import { Medias } from '../reducers/medias';
|
||||||
import { Deploys } from '../reducers/deploys';
|
import { Deploys } from '../reducers/deploys';
|
||||||
|
import { Search } from '../reducers/search';
|
||||||
|
|
||||||
export type CmsBackendType =
|
export type CmsBackendType =
|
||||||
| 'azure'
|
| 'azure'
|
||||||
@ -675,19 +676,6 @@ export type Integrations = StaticallyTypedRecord<{
|
|||||||
hooks: { [collectionOrHook: string]: any };
|
hooks: { [collectionOrHook: string]: any };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface SearchItem {
|
|
||||||
collection: string;
|
|
||||||
slug: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Search = StaticallyTypedRecord<{
|
|
||||||
entryIds?: SearchItem[];
|
|
||||||
isFetching: boolean;
|
|
||||||
term: string | null;
|
|
||||||
collections: List<string> | null;
|
|
||||||
page: number;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type Cursors = StaticallyTypedRecord<{}>;
|
export type Cursors = StaticallyTypedRecord<{}>;
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
@ -90,9 +90,8 @@ export default class RelationControl extends React.Component {
|
|||||||
forID: PropTypes.string.isRequired,
|
forID: PropTypes.string.isRequired,
|
||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
field: ImmutablePropTypes.map,
|
field: ImmutablePropTypes.map,
|
||||||
fetchID: PropTypes.string,
|
|
||||||
query: PropTypes.func.isRequired,
|
query: PropTypes.func.isRequired,
|
||||||
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
queryHits: PropTypes.array,
|
||||||
classNameWrapper: PropTypes.string.isRequired,
|
classNameWrapper: PropTypes.string.isRequired,
|
||||||
setActiveStyle: PropTypes.func.isRequired,
|
setActiveStyle: PropTypes.func.isRequired,
|
||||||
setInactiveStyle: PropTypes.func.isRequired,
|
setInactiveStyle: PropTypes.func.isRequired,
|
||||||
@ -119,7 +118,7 @@ export default class RelationControl extends React.Component {
|
|||||||
const metadata = {};
|
const metadata = {};
|
||||||
const searchFieldsArray = getSearchFieldArray(field.get('search_fields'));
|
const searchFieldsArray = getSearchFieldArray(field.get('search_fields'));
|
||||||
const { payload } = await query(forID, collection, searchFieldsArray, '', file);
|
const { payload } = await query(forID, collection, searchFieldsArray, '', file);
|
||||||
const hits = payload.response?.hits || [];
|
const hits = payload.hits || [];
|
||||||
const options = this.parseHitOptions(hits);
|
const options = this.parseHitOptions(hits);
|
||||||
const initialOptions = initialSearchValues
|
const initialOptions = initialSearchValues
|
||||||
.map(v => {
|
.map(v => {
|
||||||
@ -221,7 +220,7 @@ export default class RelationControl extends React.Component {
|
|||||||
const file = field.get('file');
|
const file = field.get('file');
|
||||||
|
|
||||||
query(forID, collection, searchFieldsArray, term, file, optionsLength).then(({ payload }) => {
|
query(forID, collection, searchFieldsArray, term, file, optionsLength).then(({ payload }) => {
|
||||||
const hits = payload.response?.hits || [];
|
const hits = payload.hits || [];
|
||||||
const options = this.parseHitOptions(hits);
|
const options = this.parseHitOptions(hits);
|
||||||
const uniq = uniqOptions(this.state.initialOptions, options);
|
const uniq = uniqOptions(this.state.initialOptions, options);
|
||||||
callback(uniq);
|
callback(uniq);
|
||||||
@ -241,8 +240,7 @@ export default class RelationControl extends React.Component {
|
|||||||
const isMultiple = this.isMultiple();
|
const isMultiple = this.isMultiple();
|
||||||
const isClearable = !field.get('required', true) || isMultiple;
|
const isClearable = !field.get('required', true) || isMultiple;
|
||||||
|
|
||||||
const hits = queryHits.get(forID, []);
|
const queryOptions = this.parseHitOptions(queryHits);
|
||||||
const queryOptions = this.parseHitOptions(hits);
|
|
||||||
const options = uniqOptions(this.state.initialOptions, queryOptions);
|
const options = uniqOptions(this.state.initialOptions, queryOptions);
|
||||||
const selectedValue = getSelectedValue({
|
const selectedValue = getSelectedValue({
|
||||||
options,
|
options,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fromJS, Map } from 'immutable';
|
import { fromJS } from 'immutable';
|
||||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { NetlifyCmsWidgetRelation } from '../';
|
import { NetlifyCmsWidgetRelation } from '../';
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ const numberFieldsHits = [
|
|||||||
class RelationController extends React.Component {
|
class RelationController extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
queryHits: Map(),
|
queryHits: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
mounted = false;
|
mounted = false;
|
||||||
@ -154,9 +154,8 @@ class RelationController extends React.Component {
|
|||||||
this.setState({ ...this.state, value });
|
this.setState({ ...this.state, value });
|
||||||
});
|
});
|
||||||
|
|
||||||
setQueryHits = jest.fn(hits => {
|
setQueryHits = jest.fn(queryHits => {
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
const queryHits = Map().set('relation-field', hits);
|
|
||||||
this.setState({ ...this.state, queryHits });
|
this.setState({ ...this.state, queryHits });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -186,7 +185,7 @@ class RelationController extends React.Component {
|
|||||||
|
|
||||||
this.setQueryHits(hits);
|
this.setQueryHits(hits);
|
||||||
|
|
||||||
return Promise.resolve({ payload: { response: { hits } } });
|
return Promise.resolve({ payload: { hits } });
|
||||||
});
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user