Merge pull request #8751 from strapi/relational-field/filter-on-relations

Enable filter on the relation fields
This commit is contained in:
cyril lopez 2020-11-30 13:55:29 +01:00 committed by GitHub
commit 62bb0bbae1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 226 additions and 39 deletions

View File

@ -19,8 +19,8 @@
"type": "string"
},
"categories": {
"collection": "category",
"via": "addresses",
"collection": "category",
"dominant": true
},
"cover": {
@ -52,4 +52,4 @@
"via": "address"
}
}
}
}

View File

@ -13,19 +13,21 @@ import {
} from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
import { formatFiltersToQuery, getTrad } from '../../utils';
import { formatFiltersToQuery, getTrad, getMainFieldType } from '../../utils';
import Container from '../Container';
import FilterPickerOption from '../FilterPickerOption';
import { Flex, Span, Wrapper } from './components';
import init from './init';
import reducer, { initialState } from './reducer';
const NOT_ALLOWED_FILTERS = ['json', 'component', 'relation', 'media', 'richtext', 'dynamiczone'];
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone'];
function FilterPicker({
contentType,
editRelations,
filters,
isOpen,
metadatas,
name,
toggleFilterPickerState,
setQuery,
@ -114,12 +116,22 @@ function FilterPicker({
let value = '';
if (type === 'boolean') {
value = 'true';
} else if (type === 'number') {
value = 0;
} else if (type === 'enumeration') {
value = get(allowedAttributes, [0, 'options', 0], '');
switch (type) {
case 'boolean': {
value = 'true';
break;
}
case 'number': {
value = 0;
break;
}
case 'enumeration': {
value = get(allowedAttributes, [0, 'options', 0], '');
break;
}
default: {
value = '';
}
}
const initFilter = {
@ -153,13 +165,39 @@ function FilterPicker({
const handleSubmit = useCallback(
e => {
e.preventDefault();
const nextFilters = formatFiltersToQuery(modifiedData);
const nextFilters = formatFiltersToQuery(modifiedData, metadatas);
emitEventRef.current('didFilterEntries');
setQuery(nextFilters);
toggleFilterPickerState();
},
[modifiedData, setQuery, toggleFilterPickerState]
[modifiedData, setQuery, toggleFilterPickerState, metadatas]
);
const handleRemoveFilter = index => {
if (index === 0 && modifiedData.length === 1) {
toggleFilterPickerState();
return;
}
dispatch({
type: 'REMOVE_FILTER',
index,
});
};
const getAttributeType = useCallback(
filter => {
const attributeType = get(contentType, ['attributes', filter.name, 'type'], '');
if (attributeType === 'relation') {
return getMainFieldType(editRelations, filter.name);
}
return attributeType;
},
[contentType, editRelations]
);
return (
@ -182,19 +220,8 @@ function FilterPicker({
modifiedData={modifiedData}
onChange={handleChange}
onClickAddFilter={addFilter}
onRemoveFilter={index => {
if (index === 0 && modifiedData.length === 1) {
toggleFilterPickerState();
return;
}
dispatch({
type: 'REMOVE_FILTER',
index,
});
}}
type={get(contentType, ['attributes', filter.name, 'type'], '')}
onRemoveFilter={handleRemoveFilter}
type={getAttributeType(filter)}
showAddButton={key === modifiedData.length - 1}
// eslint-disable-next-line react/no-array-index-key
key={key}
@ -214,13 +241,16 @@ function FilterPicker({
}
FilterPicker.defaultProps = {
editRelations: [],
name: '',
};
FilterPicker.propTypes = {
contentType: PropTypes.object.isRequired,
editRelations: PropTypes.array,
filters: PropTypes.array.isRequired,
isOpen: PropTypes.bool.isRequired,
metadatas: PropTypes.object.isRequired,
name: PropTypes.string,
setQuery: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired,

View File

@ -33,7 +33,7 @@ function FilterPickerOption({
type,
}) {
const filtersOptions = getFilterType(type);
const currentFilterName = get(modifiedData, [index, 'name']);
const currentFilterName = get(modifiedData, [index, 'name'], '');
const currentFilterData = allowedAttributes.find(attr => attr.name === currentFilterName);
const options = get(currentFilterData, ['options'], null) || ['true', 'false'];
@ -49,7 +49,7 @@ function FilterPickerOption({
onChange({ target: { name: `${index}.filter`, value: '=' } });
}}
name={`${index}.name`}
value={get(modifiedData, [index, 'name'], '')}
value={currentFilterName}
options={allowedAttributes.map(attr => attr.name)}
style={styles.select}
/>

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { get, toString } from 'lodash';
import moment from 'moment';
import { FilterButton } from 'strapi-helper-plugin';
import { dateFormats, formatFiltersToQuery } from '../../utils';
import { dateFormats, formatFiltersToQuery, getMainFieldType } from '../../utils';
function Filter({
contentType,
@ -16,7 +16,13 @@ function Filter({
isFilterPickerOpen,
setQuery,
}) {
const type = get(contentType, ['attributes', name, 'type'], 'string');
const attributeType = get(contentType, ['attributes', name, 'type'], 'string');
let type = attributeType;
if (attributeType === 'relation') {
const editRelations = get(contentType, ['layouts', 'editRelations'], []);
type = getMainFieldType(editRelations, name);
}
let displayedValue = toString(value);
if (type.includes('date') || type.includes('timestamp')) {
@ -35,9 +41,10 @@ function Filter({
.utc()
.format(format);
}
const displayedName = name.split('.')[0];
const label = {
name,
name: displayedName,
filter: filterName,
value: displayedValue,
};

View File

@ -85,6 +85,8 @@ function ListView({
const {
contentType: {
attributes,
metadatas,
layouts: { editRelations },
settings: {
defaultSortBy,
defaultSortOrder,
@ -377,6 +379,8 @@ function ListView({
contentType={contentType}
filters={filters}
isOpen={isFilterPickerOpen}
metadatas={metadatas}
editRelations={editRelations}
name={label}
toggleFilterPickerState={toggleFilterPickerState}
setQuery={setQuery}
@ -487,9 +491,11 @@ ListView.propTypes = {
components: PropTypes.object.isRequired,
contentType: PropTypes.shape({
attributes: PropTypes.object.isRequired,
metadatas: PropTypes.object.isRequired,
info: PropTypes.shape({ label: PropTypes.string.isRequired }).isRequired,
layouts: PropTypes.shape({
list: PropTypes.array.isRequired,
editRelations: PropTypes.array,
}).isRequired,
options: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,

View File

@ -8,6 +8,10 @@ const formatEditRelationsLayoutWithMetas = (obj, models) => {
const fieldSchema = get(obj, ['attributes', current], {});
const metadatas = get(obj, ['metadatas', current, 'edit'], {});
const size = 6;
const mainField = get(obj, ['metadatas', current, 'edit', 'mainField'], 'id');
const targetModelUid = get(obj, ['attributes', current, 'targetModel'], '');
const relationModel = models.find(model => model.uid === targetModelUid);
const mainFieldSchema = get(relationModel, ['attributes', mainField], {});
const queryInfos = generateRelationQueryInfos(obj, current, models);
@ -15,7 +19,7 @@ const formatEditRelationsLayoutWithMetas = (obj, models) => {
name: current,
size,
fieldSchema,
metadatas,
metadatas: { ...metadatas, mainFieldSchema },
queryInfos,
});

View File

@ -29,6 +29,11 @@ const simpleModels = [
{
uid: 'application::category.category',
isDisplayed: true,
attributes: {
name: {
type: 'string',
},
},
},
];
@ -44,6 +49,9 @@ describe('Content Manager | hooks | useFetchContentTypeLayout | utils ', () => {
},
metadatas: {
mainField: 'name',
mainFieldSchema: {
type: 'string',
},
},
queryInfos: {
endPoint: '/content-manager/relations/application::address.address/categories',

View File

@ -17,18 +17,20 @@ const VALID_REST_OPERATORS = [
// from strapi-utims/convert-rest-query-params
const findAppliedFilter = whereClause => {
// Useful to remove the mainField of relation fields.
const formattedWhereClause = whereClause.split('.')[0];
const separatorIndex = whereClause.lastIndexOf('_');
if (separatorIndex === -1) {
return { operator: '=', field: whereClause };
return { operator: '=', field: formattedWhereClause };
}
const fieldName = whereClause.substring(0, separatorIndex);
const fieldName = formattedWhereClause.substring(0, separatorIndex);
const operator = whereClause.slice(separatorIndex + 1);
// the field as underscores
if (!VALID_REST_OPERATORS.includes(operator)) {
return { operator: '=', field: whereClause };
return { operator: '=', field: formattedWhereClause };
}
return { operator: `_${operator}`, field: fieldName };

View File

@ -1,10 +1,24 @@
const formatFiltersToQuery = array => {
import { get } from 'lodash';
const formatFilterName = (name, metadatas) => {
const mainField = get(metadatas, [name, 'edit', 'mainField'], null);
if (mainField) {
return `${name}.${metadatas[name].edit.mainField}`;
}
return name;
};
const formatFiltersToQuery = (array, metadatas) => {
const nextFilters = array.map(({ name, filter, value }) => {
const formattedName = formatFilterName(name, metadatas);
if (filter === '=') {
return { [name]: value };
return { [formattedName]: value };
}
return { [`${name}${filter}`]: value };
return { [`${formattedName}${filter}`]: value };
});
return { _where: nextFilters };

View File

@ -0,0 +1,9 @@
import { get } from 'lodash';
const getMainFieldType = (editRelations, relationName) => {
const relationSchema = editRelations.find(relation => relation.name === relationName);
return get(relationSchema, ['metadatas', 'mainFieldSchema', 'type'], null);
};
export default getMainFieldType;

View File

@ -6,9 +6,10 @@ export { default as formatFiltersFromQuery } from './formatFiltersFromQuery';
export { default as formatFiltersToQuery } from './formatFiltersToQuery';
export { default as formatLayoutToApi } from './formatLayoutToApi';
export { default as generatePermissionsObject } from './generatePermissionsObject';
export { default as getInjectedComponents } from './getComponents';
export { default as getMaxTempKey } from './getMaxTempKey';
export { default as getFieldName } from './getFieldName';
export { default as getInjectedComponents } from './getComponents';
export { default as getMainFieldType } from './getMainFieldType';
export { default as getMaxTempKey } from './getMaxTempKey';
export { default as getRequestUrl } from './getRequestUrl';
export { default as getTrad } from './getTrad';
export { default as ItemTypes } from './ItemTypes';

View File

@ -3,6 +3,11 @@ import formatFiltersFromQuery, { findAppliedFilter } from '../formatFiltersFromQ
describe('CONTENT MANAGER | utils', () => {
describe('findAppliedFilter', () => {
it('should return the correct filter', () => {
expect(findAppliedFilter('categories.name')).toEqual({ operator: '=', field: 'categories' });
expect(findAppliedFilter('categories.name_lt')).toEqual({
operator: '_lt',
field: 'categories',
});
expect(findAppliedFilter('city')).toEqual({ operator: '=', field: 'city' });
expect(findAppliedFilter('city_nee')).toEqual({ operator: '=', field: 'city_nee' });
expect(findAppliedFilter('city_ne')).toEqual({ operator: '_ne', field: 'city' });
@ -30,6 +35,12 @@ describe('CONTENT MANAGER | utils', () => {
{
city: 'paris',
},
{
'categories.name_ne': 'first',
},
{
'like.numbers_lt': 34,
},
],
};
@ -37,6 +48,16 @@ describe('CONTENT MANAGER | utils', () => {
{ name: 'city_ne', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '=', value: 'paris' },
{
name: 'categories',
filter: '_ne',
value: 'first',
},
{
name: 'like',
filter: '_lt',
value: 34,
},
];
expect(formatFiltersFromQuery(query)).toEqual(expected);

View File

@ -0,0 +1,57 @@
import formatFiltersToQuery from '../formatFiltersToQuery';
describe('CONTENT MANAGER | utils', () => {
describe('formatFiltersToQuery', () => {
it('should return the filters query', () => {
const metadatas = {
categories: {
edit: {
mainField: 'name',
},
},
like: {
edit: {
mainField: 'numbers',
},
},
};
const data = [
{ name: 'city_ne', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '_ne', value: 'paris' },
{ name: 'city', filter: '=', value: 'paris' },
{
name: 'categories',
filter: '_ne',
value: 'first',
},
{
name: 'like',
filter: '_lt',
value: 34,
},
];
const expected = {
_where: [
{
city_ne_ne: 'paris',
},
{
city_ne: 'paris',
},
{
city: 'paris',
},
{
'categories.name_ne': 'first',
},
{
'like.numbers_lt': 34,
},
],
};
expect(formatFiltersToQuery(data, metadatas)).toEqual(expected);
});
});
});

View File

@ -0,0 +1,28 @@
import getMainFieldType from '../getMainFieldType';
describe('CONTENT MANAGER | UTILS | getMainFieldType', () => {
const editRelationsSchemas = [
{
name: 'categories',
metadatas: {
mainFieldSchema: {
type: 'string',
},
},
},
{
name: 'likes',
metadatas: {
mainFieldSchema: {
type: 'number',
},
},
},
];
it('should return null if the relation schema does not exist', () => {
expect(getMainFieldType(editRelationsSchemas, 'address')).toEqual(null);
});
it('should return the main field type', () => {
expect(getMainFieldType(editRelationsSchemas, 'categories')).toEqual('string');
});
});