mirror of
https://github.com/strapi/strapi.git
synced 2025-09-03 13:50:38 +00:00
Fix relation search (#9156)
* add params to relation route to omit existing relations * small refacto * use omit approach * Fix relation select (Front) Signed-off-by: HichamELBSI <elabbassih@gmail.com> * Fix relation select review Signed-off-by: HichamELBSI <elabbassih@gmail.com> * update doc aos + remove useless cast to string Co-authored-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
parent
7d19888fdd
commit
387ded4a99
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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({
|
||||
<BaselineAlignment />
|
||||
|
||||
<Component
|
||||
addRelation={value => {
|
||||
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) ? (
|
||||
|
@ -67,7 +67,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"method": "POST",
|
||||
"path": "/relations/:model/:targetField",
|
||||
"handler": "relations.find",
|
||||
"config": {
|
||||
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 = [];
|
||||
|
@ -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
|
||||
|
@ -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]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user