diff --git a/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/index.js b/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/index.js
index 09bdbc430f..32f6ce0ce1 100644
--- a/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/index.js
+++ b/packages/strapi-plugin-content-manager/admin/src/components/SelectMany/index.js
@@ -18,6 +18,7 @@ function SelectMany({
move,
onInputChange,
onMenuClose,
+ onMenuOpen,
onMenuScrollToBottom,
onRemove,
options,
@@ -84,6 +85,7 @@ function SelectMany({
onChange={addRelation}
onInputChange={onInputChange}
onMenuClose={onMenuClose}
+ onMenuOpen={onMenuOpen}
onMenuScrollToBottom={onMenuScrollToBottom}
placeholder={placeholder}
styles={styles}
@@ -140,6 +142,7 @@ SelectMany.propTypes = {
name: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
onMenuClose: PropTypes.func.isRequired,
+ onMenuOpen: PropTypes.func.isRequired,
onMenuScrollToBottom: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
diff --git a/packages/strapi-plugin-content-manager/admin/src/components/SelectOne/index.js b/packages/strapi-plugin-content-manager/admin/src/components/SelectOne/index.js
index 0a3600cae8..e38687ae2d 100644
--- a/packages/strapi-plugin-content-manager/admin/src/components/SelectOne/index.js
+++ b/packages/strapi-plugin-content-manager/admin/src/components/SelectOne/index.js
@@ -13,6 +13,7 @@ function SelectOne({
onChange,
onInputChange,
onMenuClose,
+ onMenuOpen,
onMenuScrollToBottom,
options,
placeholder,
@@ -31,6 +32,7 @@ function SelectOne({
onChange={onChange}
onInputChange={onInputChange}
onMenuClose={onMenuClose}
+ onMenuOpen={onMenuOpen}
onMenuScrollToBottom={onMenuScrollToBottom}
placeholder={placeholder}
styles={styles}
@@ -58,6 +60,7 @@ SelectOne.propTypes = {
onChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onMenuClose: PropTypes.func.isRequired,
+ onMenuOpen: PropTypes.func.isRequired,
onMenuScrollToBottom: PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
placeholder: PropTypes.node.isRequired,
diff --git a/packages/strapi-plugin-content-manager/admin/src/components/SelectWrapper/index.js b/packages/strapi-plugin-content-manager/admin/src/components/SelectWrapper/index.js
index 69f555a6f3..02028fe4fd 100644
--- a/packages/strapi-plugin-content-manager/admin/src/components/SelectWrapper/index.js
+++ b/packages/strapi-plugin-content-manager/admin/src/components/SelectWrapper/index.js
@@ -17,6 +17,11 @@ import Option from './Option';
import { A, BaselineAlignment } from './components';
import { connect, select, styles } from './utils';
+const initialPaginationState = {
+ _contains: '',
+ _limit: 20,
+ _start: 0,
+};
function SelectWrapper({
description,
editable,
@@ -34,17 +39,13 @@ function SelectWrapper({
// Disable the input in case of a polymorphic relation
const isMorph = useMemo(() => relationType.toLowerCase().includes('morph'), [relationType]);
const { addRelation, modifiedData, moveRelation, onChange, onRemoveRelation } = useDataManager();
-
const { pathname } = useLocation();
const value = get(modifiedData, name, null);
- const [state, setState] = useState({
- _contains: '',
- _limit: 20,
- _start: 0,
- });
+ const [state, setState] = useState(initialPaginationState);
const [options, setOptions] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
const filteredOptions = useMemo(() => {
return options.filter(option => {
@@ -64,6 +65,22 @@ function SelectWrapper({
const { endPoint, containsKey, defaultParams, shouldDisplayRelationLink } = queryInfos;
+ const isSingle = ['oneWay', 'oneToOne', 'manyToOne', 'oneToManyMorph', 'oneToOneMorph'].includes(
+ relationType
+ );
+
+ const idsToOmit = useMemo(() => {
+ if (!value) {
+ return [];
+ }
+
+ if (isSingle) {
+ return [value];
+ }
+
+ return value.map(val => val.id);
+ }, [isSingle, value]);
+
const getData = useCallback(
async signal => {
// Currently polymorphic relations are not handled
@@ -79,14 +96,21 @@ function SelectWrapper({
return;
}
- const params = { _limit: state._limit, _start: state._start, ...defaultParams };
+ setIsLoading(true);
+
+ const params = { _limit: state._limit, ...defaultParams };
if (state._contains) {
params[containsKey] = state._contains;
}
try {
- const data = await request(endPoint, { method: 'GET', params, signal });
+ const data = await request(endPoint, {
+ method: 'POST',
+ params,
+ signal,
+ body: { idsToOmit },
+ });
const formattedData = data.map(obj => {
return { value: obj, label: obj[mainField.name] };
@@ -108,17 +132,16 @@ function SelectWrapper({
// Silent
}
},
-
[
- containsKey,
- defaultParams,
- endPoint,
- isFieldAllowed,
isMorph,
- mainField.name,
+ isFieldAllowed,
state._limit,
- state._start,
state._contains,
+ defaultParams,
+ containsKey,
+ endPoint,
+ idsToOmit,
+ mainField.name,
]
);
@@ -126,12 +149,14 @@ function SelectWrapper({
const abortController = new AbortController();
const { signal } = abortController;
- getData(signal);
+ if (isOpen) {
+ getData(signal);
+ }
return () => abortController.abort();
- }, [getData]);
+ }, [getData, isOpen]);
- const onInputChange = (inputValue, { action }) => {
+ const handleInputChange = (inputValue, { action }) => {
if (action === 'input-change') {
setState(prevState => {
if (prevState._contains === inputValue) {
@@ -145,13 +170,26 @@ function SelectWrapper({
return inputValue;
};
- const onMenuScrollToBottom = () => {
- setState(prevState => ({ ...prevState, _start: prevState._start + 20 }));
+ const handleMenuScrollToBottom = () => {
+ setState(prevState => ({ ...prevState, _limit: prevState._limit + 20 }));
};
- const isSingle = ['oneWay', 'oneToOne', 'manyToOne', 'oneToManyMorph', 'oneToOneMorph'].includes(
- relationType
- );
+ const handleMenuClose = () => {
+ setState(initialPaginationState);
+ setIsOpen(false);
+ };
+
+ const handleChange = value => {
+ onChange({ target: { name, value: value ? value.value : value } });
+ };
+
+ const handleAddRelation = value => {
+ addRelation({ target: { name, value } });
+ };
+
+ const handleMenuOpen = () => {
+ setIsOpen(true);
+ };
const to = `/plugins/${pluginId}/collectionType/${targetModel}/${value ? value.id : null}`;
@@ -218,9 +256,7 @@ function SelectWrapper({
{
- addRelation({ target: { name, value } });
- }}
+ addRelation={handleAddRelation}
components={{ ClearIndicator, DropdownIndicator, IndicatorSeparator, Option }}
displayNavigationLink={shouldDisplayRelationLink}
id={name}
@@ -231,14 +267,11 @@ function SelectWrapper({
move={moveRelation}
name={name}
options={filteredOptions}
- onChange={value => {
- onChange({ target: { name, value: value ? value.value : value } });
- }}
- onInputChange={onInputChange}
- onMenuClose={() => {
- setState(prevState => ({ ...prevState, _contains: '' }));
- }}
- onMenuScrollToBottom={onMenuScrollToBottom}
+ onChange={handleChange}
+ onInputChange={handleInputChange}
+ onMenuClose={handleMenuClose}
+ onMenuOpen={handleMenuOpen}
+ onMenuScrollToBottom={handleMenuScrollToBottom}
onRemove={onRemoveRelation}
placeholder={
isEmpty(placeholder) ? (
diff --git a/packages/strapi-plugin-content-manager/config/routes.json b/packages/strapi-plugin-content-manager/config/routes.json
index e4fa0ef15e..0bbad348f6 100644
--- a/packages/strapi-plugin-content-manager/config/routes.json
+++ b/packages/strapi-plugin-content-manager/config/routes.json
@@ -67,7 +67,7 @@
}
},
{
- "method": "GET",
+ "method": "POST",
"path": "/relations/:model/:targetField",
"handler": "relations.find",
"config": {
diff --git a/packages/strapi-plugin-content-manager/controllers/__tests__/relations.test.js b/packages/strapi-plugin-content-manager/controllers/__tests__/relations.test.js
index 24b9e51292..5a52d858fd 100644
--- a/packages/strapi-plugin-content-manager/controllers/__tests__/relations.test.js
+++ b/packages/strapi-plugin-content-manager/controllers/__tests__/relations.test.js
@@ -158,5 +158,74 @@ describe('Relations', () => {
},
]);
});
+
+ test('Omit somes ids', async () => {
+ const result = [
+ {
+ id: 1,
+ title: 'title1',
+ secret: 'some secret',
+ },
+ {
+ id: 2,
+ title: 'title2',
+ secret: 'some secret 2',
+ },
+ ];
+ const configuration = {
+ metadatas: {
+ target: {
+ edit: {
+ mainField: 'title',
+ },
+ },
+ },
+ };
+ const assocModel = { uid: 'application::test.test', primaryKey: 'id', attributes: {} };
+ const notFound = jest.fn();
+ const find = jest.fn(() => Promise.resolve(result));
+ const findConfiguration = jest.fn(() => Promise.resolve(configuration));
+ const getModelByAssoc = jest.fn(() => assocModel);
+ const getModel = jest.fn(() => ({ attributes: { target: { model: 'test' } } }));
+
+ global.strapi = {
+ db: {
+ getModel,
+ getModelByAssoc,
+ },
+ plugins: {
+ 'content-manager': {
+ services: {
+ 'content-types': { findConfiguration },
+ 'entity-manager': { find },
+ },
+ },
+ },
+ };
+
+ const ctx = createContext(
+ {
+ params: { model: 'test', targetField: 'target' },
+ body: { idsToOmit: [3, 4] },
+ },
+ {
+ notFound,
+ }
+ );
+
+ await relations.find(ctx);
+
+ expect(find).toHaveBeenCalledWith({ _where: { id_nin: [3, 4] } }, assocModel.uid);
+ expect(ctx.body).toEqual([
+ {
+ id: 1,
+ title: 'title1',
+ },
+ {
+ id: 2,
+ title: 'title2',
+ },
+ ]);
+ });
});
});
diff --git a/packages/strapi-plugin-content-manager/controllers/relations.js b/packages/strapi-plugin-content-manager/controllers/relations.js
index 0b12acd06f..6161f58d8c 100644
--- a/packages/strapi-plugin-content-manager/controllers/relations.js
+++ b/packages/strapi-plugin-content-manager/controllers/relations.js
@@ -1,6 +1,6 @@
'use strict';
-const { has, prop, pick } = require('lodash/fp');
+const { has, prop, pick, concat } = require('lodash/fp');
const { PUBLISHED_AT_ATTRIBUTE } = require('strapi-utils').contentTypes.constants;
const { getService } = require('../utils');
@@ -9,6 +9,7 @@ module.exports = {
async find(ctx) {
const { model, targetField } = ctx.params;
const { _component, ...query } = ctx.request.query;
+ const { idsToOmit } = ctx.request.body;
if (!targetField) {
return ctx.badRequest();
@@ -31,6 +32,11 @@ module.exports = {
return ctx.notFound('target.notFound');
}
+ if (idsToOmit && Array.isArray(idsToOmit)) {
+ query._where = query._where || {};
+ query._where.id_nin = concat(query._where.id_nin || [], idsToOmit);
+ }
+
const entityManager = getService('entity-manager');
let entities = [];
diff --git a/packages/strapi-plugin-content-manager/oas.yml b/packages/strapi-plugin-content-manager/oas.yml
index 337356b7aa..f4a4d453b7 100644
--- a/packages/strapi-plugin-content-manager/oas.yml
+++ b/packages/strapi-plugin-content-manager/oas.yml
@@ -210,7 +210,7 @@ paths:
description: Suggestion if request value is not available
# Relationships
/content-manager/relations/{model}/{targetField}:
- get:
+ post:
tags:
- Relational fields
description: Fetch list of possible related content
@@ -221,12 +221,22 @@ paths:
schema:
type: string
required: true
- description: name of the field in the model that holds the relation
+ description: Name of the field in the model that holds the relation
- in: query
name: _component
schema:
type: string
description: Component uid if the targetField is in a component
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ idsToOmit:
+ type: array
+ items:
+ $ref: '#/components/schemas/id'
responses:
200:
description: Returns a list of sanitized entries based of the relational attribute info
@@ -245,7 +255,7 @@ paths:
- type: integer
'[mainField]':
type: string
- description: value of the mainField of the entry
+ description: Value of the mainField of the entry
published_at:
type: date
# Collection type
diff --git a/packages/strapi-plugin-content-manager/tests/relation-list.test.e2e.js b/packages/strapi-plugin-content-manager/tests/relation-list.test.e2e.js
index 450a823aec..532b780e5a 100644
--- a/packages/strapi-plugin-content-manager/tests/relation-list.test.e2e.js
+++ b/packages/strapi-plugin-content-manager/tests/relation-list.test.e2e.js
@@ -110,7 +110,7 @@ describe('Relation-list route', () => {
test('Can get relation-list for products of a shop', async () => {
const res = await rq({
- method: 'GET',
+ method: 'POST',
url: '/content-manager/relations/application::shop.shop/products',
});
@@ -119,6 +119,19 @@ describe('Relation-list route', () => {
expect(res.body[index]).toStrictEqual(pick(['_id', 'id', 'name'], product));
});
});
+
+ test('Can get relation-list for products of a shop and omit some results', async () => {
+ const res = await rq({
+ method: 'POST',
+ url: '/content-manager/relations/application::shop.shop/products',
+ body: {
+ idsToOmit: [data.products[0].id],
+ },
+ });
+
+ expect(res.body).toHaveLength(1);
+ expect(res.body[0]).toStrictEqual(pick(['_id', 'id', 'name'], data.products[1]));
+ });
});
describe('with draftAndPublish', () => {
@@ -145,7 +158,7 @@ describe('Relation-list route', () => {
test('Can get relation-list for products of a shop', async () => {
const res = await rq({
- method: 'GET',
+ method: 'POST',
url: '/content-manager/relations/application::shop.shop/products',
});
@@ -161,5 +174,18 @@ describe('Relation-list route', () => {
published_at: null,
});
});
+
+ test('Can get relation-list for products of a shop and omit some results', async () => {
+ const res = await rq({
+ method: 'POST',
+ url: '/content-manager/relations/application::shop.shop/products',
+ body: {
+ idsToOmit: [data.products[1].id],
+ },
+ });
+
+ expect(res.body).toHaveLength(1);
+ expect(res.body[0]).toMatchObject(pick(['_id', 'id', 'name'], data.products[0]));
+ });
});
});