diff --git a/examples/getstarted/api/address/models/Address.settings.json b/examples/getstarted/api/address/models/Address.settings.json index abd06b8bcb..8aae91bc97 100755 --- a/examples/getstarted/api/address/models/Address.settings.json +++ b/examples/getstarted/api/address/models/Address.settings.json @@ -19,8 +19,8 @@ "type": "string" }, "categories": { - "collection": "category", "via": "addresses", + "collection": "category", "dominant": true }, "cover": { @@ -52,4 +52,4 @@ "via": "address" } } -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-content-manager/admin/src/components/FilterPicker/index.js b/packages/strapi-plugin-content-manager/admin/src/components/FilterPicker/index.js index 1492f90637..5eea17569d 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/FilterPicker/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/FilterPicker/index.js @@ -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, diff --git a/packages/strapi-plugin-content-manager/admin/src/components/FilterPickerOption/index.js b/packages/strapi-plugin-content-manager/admin/src/components/FilterPickerOption/index.js index d125c53a97..7905af134c 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/FilterPickerOption/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/FilterPickerOption/index.js @@ -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} /> diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/Filter.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/Filter.js index 7819a91a11..a56b8b3732 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/Filter.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/Filter.js @@ -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, }; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js index 02279e4e4a..3d4e95ddff 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js @@ -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, diff --git a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js index 9d7a6d9d87..4babf0c1ab 100644 --- a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js +++ b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js @@ -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, }); diff --git a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/tests/formatLayouts.test.js b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/tests/formatLayouts.test.js index 3315ef5c82..568909f0f4 100644 --- a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/tests/formatLayouts.test.js +++ b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/tests/formatLayouts.test.js @@ -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', diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersFromQuery.js b/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersFromQuery.js index 12874a714a..46c856f3b2 100644 --- a/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersFromQuery.js +++ b/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersFromQuery.js @@ -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 }; diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersToQuery.js b/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersToQuery.js index d19262947b..442b5aa5fd 100644 --- a/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersToQuery.js +++ b/packages/strapi-plugin-content-manager/admin/src/utils/formatFiltersToQuery.js @@ -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 }; diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/getMainFieldType.js b/packages/strapi-plugin-content-manager/admin/src/utils/getMainFieldType.js new file mode 100644 index 0000000000..4f71e32e2f --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/utils/getMainFieldType.js @@ -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; diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/index.js b/packages/strapi-plugin-content-manager/admin/src/utils/index.js index a4f66f4231..8e7040e825 100644 --- a/packages/strapi-plugin-content-manager/admin/src/utils/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/utils/index.js @@ -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'; diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFilterFromQuery.test.js b/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFilterFromQuery.test.js index 5203a67b79..a1b21305cd 100644 --- a/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFilterFromQuery.test.js +++ b/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFilterFromQuery.test.js @@ -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); diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFiltersToQuery.test.js b/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFiltersToQuery.test.js new file mode 100644 index 0000000000..6090ded717 --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/utils/tests/formatFiltersToQuery.test.js @@ -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); + }); + }); +}); diff --git a/packages/strapi-plugin-content-manager/admin/src/utils/tests/getMainFieldType.test.js b/packages/strapi-plugin-content-manager/admin/src/utils/tests/getMainFieldType.test.js new file mode 100644 index 0000000000..1a3f36274c --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/utils/tests/getMainFieldType.test.js @@ -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'); + }); +});