feat: add filter to collection view (#3741)
This commit is contained in:
@ -4,6 +4,7 @@ import reducer, {
|
||||
selectMediaFolder,
|
||||
selectMediaFilePath,
|
||||
selectMediaFilePublicPath,
|
||||
selectEntries,
|
||||
} from '../entries';
|
||||
|
||||
const initialState = OrderedMap({
|
||||
@ -559,4 +560,118 @@ describe('entries', () => {
|
||||
).toBe('/images/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectEntries', () => {
|
||||
it('should return all entries', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1' },
|
||||
'posts.2': { slug: '2' },
|
||||
'posts.3': { slug: '3' },
|
||||
'posts.4': { slug: '4' },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([{ slug: '1' }, { slug: '2' }, { slug: '3' }, { slug: '4' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return sorted entries entries by field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1' } },
|
||||
'posts.2': { slug: '2', data: { title: '2' } },
|
||||
'posts.3': { slug: '3', data: { title: '3' } },
|
||||
'posts.4': { slug: '4', data: { title: '4' } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
sort: { posts: { title: { key: 'title', direction: 'Descending' } } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([
|
||||
{ slug: '4', data: { title: '4' } },
|
||||
{ slug: '3', data: { title: '3' } },
|
||||
{ slug: '2', data: { title: '2' } },
|
||||
{ slug: '1', data: { title: '1' } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return sorted entries entries by nested field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1', nested: { date: 4 } } },
|
||||
'posts.2': { slug: '2', data: { title: '2', nested: { date: 3 } } },
|
||||
'posts.3': { slug: '3', data: { title: '3', nested: { date: 2 } } },
|
||||
'posts.4': { slug: '4', data: { title: '4', nested: { date: 1 } } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
sort: { posts: { title: { key: 'nested.date', direction: 'Ascending' } } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([
|
||||
{ slug: '4', data: { title: '4', nested: { date: 1 } } },
|
||||
{ slug: '3', data: { title: '3', nested: { date: 2 } } },
|
||||
{ slug: '2', data: { title: '2', nested: { date: 3 } } },
|
||||
{ slug: '1', data: { title: '1', nested: { date: 4 } } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return filtered entries entries by field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1' } },
|
||||
'posts.2': { slug: '2', data: { title: '2' } },
|
||||
'posts.3': { slug: '3', data: { title: '3' } },
|
||||
'posts.4': { slug: '4', data: { title: '4' } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
filter: { posts: { title__1: { field: 'title', pattern: '4', active: true } } },
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(fromJS([{ slug: '4', data: { title: '4' } }]));
|
||||
});
|
||||
|
||||
it('should return filtered entries entries by nested field', () => {
|
||||
const state = fromJS({
|
||||
entities: {
|
||||
'posts.1': { slug: '1', data: { title: '1', nested: { draft: true } } },
|
||||
'posts.2': { slug: '2', data: { title: '2', nested: { draft: true } } },
|
||||
'posts.3': { slug: '3', data: { title: '3', nested: { draft: false } } },
|
||||
'posts.4': { slug: '4', data: { title: '4', nested: { draft: false } } },
|
||||
},
|
||||
pages: { posts: { ids: ['1', '2', '3', '4'] } },
|
||||
filter: {
|
||||
posts: { 'nested.draft__false': { field: 'nested.draft', pattern: false, active: true } },
|
||||
},
|
||||
});
|
||||
const collection = fromJS({
|
||||
name: 'posts',
|
||||
});
|
||||
|
||||
expect(selectEntries(state, collection)).toEqual(
|
||||
fromJS([
|
||||
{ slug: '3', data: { title: '3', nested: { draft: false } } },
|
||||
{ slug: '4', data: { title: '4', nested: { draft: false } } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
EntryField,
|
||||
State,
|
||||
EntryMap,
|
||||
ViewFilter,
|
||||
} from '../types/redux';
|
||||
import { selectMediaFolder } from './entries';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
@ -423,6 +424,11 @@ export const selectSortDataPath = (collection: Collection, key: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const selectViewFilters = (collection: Collection) => {
|
||||
const viewFilters = collection.get('view_filters').toJS() as ViewFilter[];
|
||||
return viewFilters;
|
||||
};
|
||||
|
||||
export const selectFieldsComments = (collection: Collection, entryMap: EntryMap) => {
|
||||
let fields: EntryField[] = [];
|
||||
if (collection.has('folder')) {
|
||||
|
@ -11,6 +11,9 @@ import {
|
||||
SORT_ENTRIES_REQUEST,
|
||||
SORT_ENTRIES_SUCCESS,
|
||||
SORT_ENTRIES_FAILURE,
|
||||
FILTER_ENTRIES_REQUEST,
|
||||
FILTER_ENTRIES_SUCCESS,
|
||||
FILTER_ENTRIES_FAILURE,
|
||||
} from '../actions/entries';
|
||||
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
|
||||
import {
|
||||
@ -30,16 +33,23 @@ import {
|
||||
EntryField,
|
||||
CollectionFiles,
|
||||
EntriesSortRequestPayload,
|
||||
EntriesSortSuccessPayload,
|
||||
EntriesSortFailurePayload,
|
||||
SortMap,
|
||||
SortObject,
|
||||
Sort,
|
||||
SortDirection,
|
||||
Filter,
|
||||
FilterMap,
|
||||
EntriesFilterRequestPayload,
|
||||
EntriesFilterFailurePayload,
|
||||
} from '../types/redux';
|
||||
import { folderFormatter } from '../lib/formatters';
|
||||
import { isAbsolutePath, basename } from 'netlify-cms-lib-util';
|
||||
import { trim, once, sortBy, set } from 'lodash';
|
||||
import { trim, once, sortBy, set, orderBy } from 'lodash';
|
||||
import { selectSortDataPath } from './collections';
|
||||
import { stringTemplate } from 'netlify-cms-lib-widgets';
|
||||
|
||||
const { keyToPathArray } = stringTemplate;
|
||||
|
||||
let collection: string;
|
||||
let loadedEntries: EntryObject[];
|
||||
@ -203,8 +213,9 @@ const entries = (
|
||||
return newState;
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_SUCCESS:
|
||||
case SORT_ENTRIES_SUCCESS: {
|
||||
const payload = action.payload as EntriesSortSuccessPayload;
|
||||
const payload = action.payload as { collection: string; entries: EntryObject[] };
|
||||
const { collection, entries } = payload;
|
||||
loadedEntries = entries;
|
||||
const newState = state.withMutations(map => {
|
||||
@ -238,6 +249,29 @@ const entries = (
|
||||
return newState;
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_REQUEST: {
|
||||
const payload = action.payload as EntriesFilterRequestPayload;
|
||||
const { collection, filter } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
const current: FilterMap = map.getIn(['filter', collection, filter.id], fromJS(filter));
|
||||
map.setIn(
|
||||
['filter', collection, current.get('id')],
|
||||
current.set('active', !current.get('active')),
|
||||
);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_FAILURE: {
|
||||
const payload = action.payload as EntriesFilterFailurePayload;
|
||||
const { collection, filter } = payload;
|
||||
const newState = state.withMutations(map => {
|
||||
map.deleteIn(['filter', collection, filter.id]);
|
||||
map.setIn(['pages', collection, 'isFetching'], false);
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@ -248,6 +282,11 @@ export const selectEntriesSort = (entries: Entries, collection: string) => {
|
||||
return sort?.get(collection);
|
||||
};
|
||||
|
||||
export const selectEntriesFilter = (entries: Entries, collection: string) => {
|
||||
const filter = entries.get('filter') as Filter | undefined;
|
||||
return filter?.get(collection) || Map();
|
||||
};
|
||||
|
||||
export const selectEntriesSortFields = (entries: Entries, collection: string) => {
|
||||
const sort = selectEntriesSort(entries, collection);
|
||||
const values =
|
||||
@ -255,6 +294,17 @@ export const selectEntriesSortFields = (entries: Entries, collection: string) =>
|
||||
?.valueSeq()
|
||||
.filter(v => v?.get('direction') !== SortDirection.None)
|
||||
.toArray() || [];
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
export const selectEntriesFilterFields = (entries: Entries, collection: string) => {
|
||||
const filter = selectEntriesFilter(entries, collection);
|
||||
const values =
|
||||
filter
|
||||
?.valueSeq()
|
||||
.filter(v => v?.get('active') === true)
|
||||
.toArray() || [];
|
||||
return values;
|
||||
};
|
||||
|
||||
@ -264,10 +314,39 @@ export const selectEntry = (state: Entries, collection: string, slug: string) =>
|
||||
export const selectPublishedSlugs = (state: Entries, collection: string) =>
|
||||
state.getIn(['pages', collection, 'ids'], List<string>());
|
||||
|
||||
export const selectEntries = (state: Entries, collection: string) => {
|
||||
const slugs = selectPublishedSlugs(state, collection);
|
||||
const entries =
|
||||
slugs && (slugs.map(slug => selectEntry(state, collection, slug as string)) as List<EntryMap>);
|
||||
export const selectEntries = (state: Entries, collection: Collection) => {
|
||||
const collectionName = collection.get('name');
|
||||
const slugs = selectPublishedSlugs(state, collectionName);
|
||||
let entries =
|
||||
slugs &&
|
||||
(slugs.map(slug => selectEntry(state, collectionName, slug as string)) as List<EntryMap>);
|
||||
|
||||
const sortFields = selectEntriesSortFields(state, collectionName);
|
||||
if (sortFields && sortFields.length > 0) {
|
||||
const keys = sortFields.map(v => selectSortDataPath(collection, v.get('key')));
|
||||
const orders = sortFields.map(v =>
|
||||
v.get('direction') === SortDirection.Ascending ? 'asc' : 'desc',
|
||||
);
|
||||
entries = fromJS(orderBy(entries.toJS(), keys, orders));
|
||||
}
|
||||
|
||||
const filters = selectEntriesFilterFields(state, collectionName);
|
||||
if (filters && filters.length > 0) {
|
||||
entries = entries
|
||||
.filter(e => {
|
||||
const allMatched = filters.every(f => {
|
||||
const pattern = f.get('pattern');
|
||||
const field = f.get('field');
|
||||
const data = e!.get('data') || Map();
|
||||
const toMatch = data.getIn(keyToPathArray(field));
|
||||
const matched =
|
||||
toMatch !== undefined && new RegExp(String(pattern)).test(String(toMatch));
|
||||
return matched;
|
||||
});
|
||||
return allMatched;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import mediaLibrary from './mediaLibrary';
|
||||
import deploys, * as fromDeploys from './deploys';
|
||||
import globalUI from './globalUI';
|
||||
import { Status } from '../constants/publishModes';
|
||||
import { State } from '../types/redux';
|
||||
import { State, Collection } from '../types/redux';
|
||||
|
||||
const reducers = {
|
||||
auth,
|
||||
@ -38,7 +38,7 @@ export default reducers;
|
||||
export const selectEntry = (state: State, collection: string, slug: string) =>
|
||||
fromEntries.selectEntry(state.entries, collection, slug);
|
||||
|
||||
export const selectEntries = (state: State, collection: string) =>
|
||||
export const selectEntries = (state: State, collection: Collection) =>
|
||||
fromEntries.selectEntries(state.entries, collection);
|
||||
|
||||
export const selectPublishedSlugs = (state: State, collection: string) =>
|
||||
|
Reference in New Issue
Block a user