mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 03:17:11 +00:00
Merge pull request #8751 from strapi/relational-field/filter-on-relations
Enable filter on the relation fields
This commit is contained in:
commit
62bb0bbae1
@ -19,8 +19,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"categories": {
|
||||
"collection": "category",
|
||||
"via": "addresses",
|
||||
"collection": "category",
|
||||
"dominant": true
|
||||
},
|
||||
"cover": {
|
||||
@ -52,4 +52,4 @@
|
||||
"via": "address"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user