Merge pull request #15900 from strapi/fix/relations-caching-between-locales

This commit is contained in:
Josh 2023-03-03 15:42:28 +00:00 committed by GitHub
commit 3234fcc61d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 153 additions and 69 deletions

View File

@ -147,7 +147,7 @@ The input field for relation fields consist of two components:
### `RelationInputDataManager`
This container component handles data fetching and data normalization for the `RelationInput` component. This has been extracted from
This container component handles data fetching and data normalization for the `RelationInput` component. This has been extracted from
the `RelationInput` so that Strapi is able to move the underlying component into the design-system if the community would need it
(most other input components can be consumed from there).
@ -163,7 +163,7 @@ would otherwise have a negative impact on the overall performance of the content
This hook takes care of data-fetching and normalizes results relations aswell as search-results.
```ts
const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: string, options: Options);
const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: Array<string | object>, options: Options);
```
### `Options`
@ -205,10 +205,10 @@ type RelationResults = RelationResult[];
type RelationResult = {
id: number;
href?: string; // based on `shouldAddLink` and the `targetModel`
href?: string; // based on `shouldAddLink` and the `targetModel`
publicationState: 'draft' | 'published';
mainField: string; // will fallback to "id" if not set
}
mainField: string; // will fallback to "id" if not set
};
```
#### `relations`

View File

@ -53,6 +53,12 @@
}
},
"type": "date"
},
"relation_locales": {
"type": "relation",
"relation": "manyToMany",
"target": "api::relation-locale.relation-locale",
"mappedBy": "categories"
}
}
}

View File

@ -0,0 +1,56 @@
{
"kind": "collectionType",
"collectionName": "relation_locales",
"info": {
"singularName": "relation-locale",
"pluralName": "relation-locales",
"displayName": "Relations",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"categories": {
"type": "relation",
"relation": "manyToMany",
"target": "api::category.category",
"inversedBy": "relation_locales"
},
"title": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"repeatable_relations": {
"type": "component",
"repeatable": true,
"pluginOptions": {
"i18n": {
"localized": true
}
},
"component": "basic.relation"
},
"dynamic_relations": {
"pluginOptions": {
"i18n": {
"localized": true
}
},
"type": "dynamiczone",
"components": [
"basic.relation",
"basic.simple"
]
}
}
}

View File

@ -0,0 +1,9 @@
'use strict';
/**
* relation-locale controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::relation-locale.relation-locale');

View File

@ -0,0 +1,9 @@
'use strict';
/**
* relation-locale router
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::relation-locale.relation-locale');

View File

@ -0,0 +1,9 @@
'use strict';
/**
* relation-locale service
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::relation-locale.relation-locale');

View File

@ -156,11 +156,13 @@ const reducer = (state, action) =>
const initialDataRelations = get(state, initialDataPath);
const modifiedDataRelations = get(state, modifiedDataPath);
const valuesToLoad = value.filter((relation) => {
return !initialDataRelations.some((initialDataRelation) => {
return initialDataRelation.id === relation.id;
});
});
const valuesToLoad = !initialDataRelations
? value
: value.filter((relation) => {
return !initialDataRelations.some((initialDataRelation) => {
return initialDataRelation.id === relation.id;
});
});
const keys = generateNKeysBetween(
null,
@ -282,9 +284,7 @@ const reducer = (state, action) =>
* relationalFieldPaths won't be an array which is what we're expecting
* Therefore we reset these bits of state to the correct data type
* which is an array. Hence why we replace those fields.
*
*/
const mergeDataWithPreparedRelations = relationalFieldPaths
.map((path) => path.split('.'))
.reduce((acc, currentPaths) => {
@ -301,7 +301,14 @@ const reducer = (state, action) =>
return result;
}, existingComponents);
if (state.modifiedData && get(state.modifiedData, componentName)) {
if (
state.modifiedData &&
get(state.modifiedData, componentName) &&
/**
* Only replace the state if the entity ids are the same.
*/
state.modifiedData?.id === initialValues?.id
) {
/**
* this will be null on initial load, however subsequent calls
* will have data in them correlating to the names of the relational fields.

View File

@ -107,8 +107,7 @@ const RelationInput = ({
[totalNumberOfRelations, numberOfRelationsToDisplay]
);
const shouldDisplayLoadMoreButton =
(!!labelLoadMore && paginatedRelations.hasNextPage) || paginatedRelations.isLoading;
const shouldDisplayLoadMoreButton = !!labelLoadMore && paginatedRelations.hasNextPage;
const options = useMemo(
() =>

View File

@ -295,23 +295,6 @@ describe('Content-Manager || RelationInput', () => {
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
});
test('should display load more button loading if there is no next page but loading is true', () => {
setup({
relations: {
data: [],
isLoading: true,
isSuccess: true,
hasNextPage: false,
isFetchingNextPage: false,
},
});
expect(screen.getByRole('button', { name: 'Load more' })).toHaveAttribute(
'aria-disabled',
'true'
);
});
test('should display error state', () => {
setup({ error: 'This is an error' });

View File

@ -57,41 +57,47 @@ export const RelationInputDataManager = ({
const currentLastPage = Math.ceil(get(initialData, name, []).length / RELATIONS_TO_DISPLAY);
const cacheKey = `${slug}-${initialDataPath.join('.')}`;
const { relations, search, searchFor } = useRelation(cacheKey, {
relation: {
enabled: !!endpoints.relation,
endpoint: endpoints.relation,
pageGoal: currentLastPage,
pageParams: {
...defaultParams,
pageSize: RELATIONS_TO_DISPLAY,
const { relations, search, searchFor } = useRelation(
[slug, initialDataPath.join('.'), modifiedData.id, defaultParams],
{
relation: {
enabled: !!endpoints.relation,
endpoint: endpoints.relation,
pageGoal: currentLastPage,
pageParams: {
...defaultParams,
pageSize: RELATIONS_TO_DISPLAY,
},
onLoad(value) {
relationLoad({
target: {
initialDataPath: ['initialData', ...initialDataPath],
modifiedDataPath: ['modifiedData', ...nameSplit],
value,
},
});
},
normalizeArguments: {
mainFieldName: mainField.name,
shouldAddLink: shouldDisplayRelationLink,
targetModel,
},
},
onLoad(value) {
relationLoad({
target: {
initialDataPath: ['initialData', ...initialDataPath],
modifiedDataPath: ['modifiedData', ...nameSplit],
value,
},
});
search: {
endpoint: endpoints.search,
pageParams: {
...defaultParams,
// eslint-disable-next-line no-nested-ternary
entityId: isCreatingEntry
? undefined
: isComponentRelation
? componentId
: initialData.id,
pageSize: SEARCH_RESULTS_TO_DISPLAY,
},
},
normalizeArguments: {
mainFieldName: mainField.name,
shouldAddLink: shouldDisplayRelationLink,
targetModel,
},
},
search: {
endpoint: endpoints.search,
pageParams: {
...defaultParams,
// eslint-disable-next-line no-nested-ternary
entityId: isCreatingEntry ? undefined : isComponentRelation ? componentId : initialData.id,
pageSize: SEARCH_RESULTS_TO_DISPLAY,
},
},
});
}
);
const isMorph = useMemo(() => relationType.toLowerCase().includes('morph'), [relationType]);
const toOneRelation = [

View File

@ -189,7 +189,7 @@ describe('RelationInputDataManager', () => {
});
expect(useRelation).toBeCalledWith(
expect.any(String),
expect.arrayContaining([expect.any(String)]),
expect.objectContaining({
search: expect.objectContaining({
pageParams: expect.objectContaining({

View File

@ -33,7 +33,7 @@ const ComponentFixture = ({ children }) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
);
const cacheKey = 'useRelation-cache-key';
const cacheKey = ['useRelation-cache-key'];
function setup(args) {
return new Promise((resolve) => {
act(() => {

View File

@ -49,7 +49,7 @@ export const useRelation = (cacheKey, { relation, search }) => {
const { onLoad: onLoadRelations, normalizeArguments = {} } = relation;
const relationsRes = useInfiniteQuery(['relation', cacheKey], fetchRelations, {
const relationsRes = useInfiniteQuery(['relation', ...cacheKey], fetchRelations, {
cacheTime: 0,
enabled: relation.enabled,
/**
@ -138,7 +138,7 @@ export const useRelation = (cacheKey, { relation, search }) => {
}, [status, onLoadRelationsCallback, data]);
const searchRes = useInfiniteQuery(
['relation', cacheKey, 'search', JSON.stringify(searchParams)],
['relation', ...cacheKey, 'search', JSON.stringify(searchParams)],
fetchSearch,
{
enabled: Object.keys(searchParams).length > 0,