feat: Add group by to collection view (Issue 3614) (#4486)

This commit is contained in:
Kancer (Nilay) Gökırmak
2020-11-08 17:33:09 +01:00
committed by GitHub
parent 519cb2d4c2
commit e52e29034e
22 changed files with 537 additions and 110 deletions

View File

@ -13,6 +13,7 @@ import {
State,
EntryMap,
ViewFilter,
ViewGroup,
} from '../types/redux';
import { selectMediaFolder } from './entries';
import { stringTemplate } from 'netlify-cms-lib-widgets';
@ -430,6 +431,11 @@ export const selectViewFilters = (collection: Collection) => {
return viewFilters;
};
export const selectViewGroups = (collection: Collection) => {
const viewGroups = collection.get('view_groups').toJS() as ViewGroup[];
return viewGroups;
};
export const selectFieldsComments = (collection: Collection, entryMap: EntryMap) => {
let fields: EntryField[] = [];
if (collection.has('folder')) {

View File

@ -1,6 +1,11 @@
import { fromJS } from 'immutable';
import { Cursor } from 'netlify-cms-lib-util';
import { ENTRIES_SUCCESS, SORT_ENTRIES_SUCCESS } from 'Actions/entries';
import {
ENTRIES_SUCCESS,
SORT_ENTRIES_SUCCESS,
FILTER_ENTRIES_SUCCESS,
GROUP_ENTRIES_SUCCESS,
} from 'Actions/entries';
// Since pagination can be used for a variety of views (collections
// and searches are the most common examples), we namespace cursors by
@ -16,6 +21,8 @@ const cursors = (state = fromJS({ cursorsByType: { collectionEntries: {} } }), a
Cursor.create(action.payload.cursor).store,
);
}
case FILTER_ENTRIES_SUCCESS:
case GROUP_ENTRIES_SUCCESS:
case SORT_ENTRIES_SUCCESS: {
return state.deleteIn(['cursorsByType', 'collectionEntries', action.payload.collection]);
}

View File

@ -1,4 +1,4 @@
import { Map, List, fromJS, OrderedMap } from 'immutable';
import { Map, List, fromJS, OrderedMap, Set } from 'immutable';
import { dirname, join } from 'path';
import {
ENTRY_REQUEST,
@ -14,6 +14,9 @@ import {
FILTER_ENTRIES_REQUEST,
FILTER_ENTRIES_SUCCESS,
FILTER_ENTRIES_FAILURE,
GROUP_ENTRIES_REQUEST,
GROUP_ENTRIES_SUCCESS,
GROUP_ENTRIES_FAILURE,
CHANGE_VIEW_STYLE,
} from '../actions/entries';
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
@ -40,14 +43,19 @@ import {
Sort,
SortDirection,
Filter,
Group,
FilterMap,
GroupMap,
EntriesFilterRequestPayload,
EntriesFilterFailurePayload,
ChangeViewStylePayload,
EntriesGroupRequestPayload,
EntriesGroupFailurePayload,
GroupOfEntries,
} from '../types/redux';
import { folderFormatter } from '../lib/formatters';
import { isAbsolutePath, basename } from 'netlify-cms-lib-util';
import { trim, once, sortBy, set, orderBy } from 'lodash';
import { trim, once, sortBy, set, orderBy, groupBy } from 'lodash';
import { selectSortDataPath } from './collections';
import { stringTemplate } from 'netlify-cms-lib-widgets';
import { VIEW_STYLE_LIST } from '../constants/collectionViews';
@ -239,6 +247,7 @@ const entries = (
return newState;
}
case GROUP_ENTRIES_SUCCESS:
case FILTER_ENTRIES_SUCCESS:
case SORT_ENTRIES_SUCCESS: {
const payload = action.payload as { collection: string; entries: EntryObject[] };
@ -298,6 +307,30 @@ const entries = (
return newState;
}
case GROUP_ENTRIES_REQUEST: {
const payload = action.payload as EntriesGroupRequestPayload;
const { collection, group } = payload;
const newState = state.withMutations(map => {
const current: GroupMap = map.getIn(['group', collection, group.id], fromJS(group));
map.deleteIn(['group', collection]);
map.setIn(
['group', collection, current.get('id')],
current.set('active', !current.get('active')),
);
});
return newState;
}
case GROUP_ENTRIES_FAILURE: {
const payload = action.payload as EntriesGroupFailurePayload;
const { collection, group } = payload;
const newState = state.withMutations(map => {
map.deleteIn(['group', collection, group.id]);
map.setIn(['pages', collection, 'isFetching'], false);
});
return newState;
}
case CHANGE_VIEW_STYLE: {
const payload = (action.payload as unknown) as ChangeViewStylePayload;
const { style } = payload;
@ -323,6 +356,17 @@ export const selectEntriesFilter = (entries: Entries, collection: string) => {
return filter?.get(collection) || Map();
};
export const selectEntriesGroup = (entries: Entries, collection: string) => {
const group = entries.get('group') as Group | undefined;
return group?.get(collection) || Map();
};
export const selectEntriesGroupField = (entries: Entries, collection: string) => {
const groups = selectEntriesGroup(entries, collection);
const value = groups?.valueSeq().find(v => v?.get('active') === true);
return value;
};
export const selectEntriesSortFields = (entries: Entries, collection: string) => {
const sort = selectEntriesSort(entries, collection);
const values =
@ -354,12 +398,17 @@ 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: Collection) => {
const collectionName = collection.get('name');
const getPublishedEntries = (state: Entries, collectionName: string) => {
const slugs = selectPublishedSlugs(state, collectionName);
let entries =
const entries =
slugs &&
(slugs.map(slug => selectEntry(state, collectionName, slug as string)) as List<EntryMap>);
return entries;
};
export const selectEntries = (state: Entries, collection: Collection) => {
const collectionName = collection.get('name');
let entries = getPublishedEntries(state, collectionName);
const sortFields = selectEntriesSortFields(state, collectionName);
if (sortFields && sortFields.length > 0) {
@ -391,6 +440,75 @@ export const selectEntries = (state: Entries, collection: Collection) => {
return entries;
};
const getGroup = (entry: EntryMap, selectedGroup: GroupMap) => {
const label = selectedGroup.get('label');
const field = selectedGroup.get('field');
const fieldData = entry.getIn(['data', ...keyToPathArray(field)]);
if (fieldData === undefined) {
return {
id: 'missing_value',
label,
value: fieldData,
};
}
const dataAsString = String(fieldData);
if (selectedGroup.has('pattern')) {
const pattern = selectedGroup.get('pattern');
let value = '';
try {
const regex = new RegExp(pattern);
const matched = dataAsString.match(regex);
if (matched) {
value = matched[0];
}
} catch (e) {
console.warn(`Invalid view group pattern '${pattern}' for field '${field}'`, e);
}
return {
id: `${label}${value}`,
label,
value,
};
}
return {
id: `${label}${fieldData}`,
label,
value: typeof fieldData === 'boolean' ? fieldData : dataAsString,
};
};
export const selectGroups = (state: Entries, collection: Collection) => {
const collectionName = collection.get('name');
const entries = getPublishedEntries(state, collectionName);
const selectedGroup = selectEntriesGroupField(state, collectionName);
if (selectedGroup === undefined) {
return [];
}
let groups: Record<
string,
{ id: string; label: string; value: string | boolean | undefined }
> = {};
const groupedEntries = groupBy(entries.toArray(), entry => {
const group = getGroup(entry, selectedGroup);
groups = { ...groups, [group.id]: group };
return group.id;
});
const groupsArray: GroupOfEntries[] = Object.entries(groupedEntries).map(([id, entries]) => {
return {
...groups[id],
paths: Set(entries.map(entry => entry.get('path'))),
};
});
return groupsArray;
};
export const selectEntryByPath = (state: Entries, collection: string, path: string) => {
const slugs = selectPublishedSlugs(state, collection);
const entries =