Merge pull request #14768 from strapi/fix/relations/re-add-count

feat: re-add the count & make it dynamic
This commit is contained in:
Gustav Hansen 2022-11-03 11:50:12 +01:00 committed by GitHub
commit 879d051f9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 316 additions and 50 deletions

View File

@ -5,6 +5,7 @@ import set from 'lodash/set';
import take from 'lodash/take';
import cloneDeep from 'lodash/cloneDeep';
import uniqBy from 'lodash/uniqBy';
import merge from 'lodash/merge';
import {
findLeafByPathAndReplace,
@ -217,8 +218,12 @@ const reducer = (state, action) =>
/**
* this will be null on initial load, however subsequent calls
* will have data in them correlating to the names of the relational fields.
*
* We also merge the fetched data so that things like `id` for components can be copied over
* which would be `undefined` in the `browserState`.
*/
set(acc, componentName, get(state.modifiedData, componentName));
const currentState = cloneDeep(get(state.modifiedData, componentName));
set(acc, componentName, merge(currentState, get(initialValues, componentName)));
} else if (
repeatableComponentPaths.includes(componentName) ||
dynamicZonePaths.includes(componentName) ||

View File

@ -1600,6 +1600,60 @@ describe('CONTENT MANAGER | COMPONENTS | EditViewDataManagerProvider | reducer',
expect(reducer(state, action)).toEqual(expected);
});
});
it('should merge modifiedData with relation containing fields if the modifiedData exists', () => {
const state = {
...initialState,
formErrors: true,
initialData: {},
modifiedData: {
relation: [
{
id: 1,
},
],
componentWithRelation: {
relation: [
{
id: 1,
},
{
id: 2,
},
],
},
},
modifiedDZName: true,
shouldCheckErrors: true,
};
const action = {
type: 'INIT_FORM',
initialValues: {
ok: true,
relation: { count: 10 },
componentWithRelation: {
id: 1,
relation: {
count: 10,
},
},
},
relationalFieldPaths: ['relation', 'componentWithRelation.relation'],
componentPaths: ['componentWithRelation'],
};
const newState = reducer(state, action);
expect(newState.modifiedData.relation[0]).toEqual({
id: 1,
});
expect(newState.modifiedData.componentWithRelation).toEqual({
id: 1,
relation: expect.arrayContaining([{ id: expect.any(Number) }]),
});
});
});
describe('MOVE_COMPONENT_FIELD', () => {

View File

@ -98,15 +98,14 @@ const RelationInput = ({
const options = useMemo(
() =>
data.flat().map((result) =>
result
? {
...result,
value: result.id,
label: result.mainField,
}
: result
),
data
.flat()
.filter(Boolean)
.map((result) => ({
...result,
value: result.id,
label: result.mainField,
})),
[data]
);

View File

@ -47,7 +47,7 @@ export const RelationInputDataManager = ({
const { relations, search, searchFor } = useRelation(`${slug}-${name}-${initialData?.id ?? ''}`, {
name,
relation: {
enabled: get(initialData, name)?.count !== 0 && !!endpoints.relation,
enabled: !!endpoints.relation,
endpoint: endpoints.relation,
pageGoal: currentLastPage,
pageParams: {
@ -138,6 +138,28 @@ export const RelationInputDataManager = ({
return <NotAllowedInput name={name} intlLabel={intlLabel} labelAction={labelAction} />;
}
/**
* How to calculate the total number of relations even if you don't
* have them all loaded in the browser.
*
* 1. The `infiniteQuery` gives you the total number of relations in the pagination result.
* 2. You can diff the length of the browserState vs the fetchedServerState to determine if you've
* either added or removed relations.
* 3. Add them together, if you've removed relations you'll get a negative number and it'll
* actually subtract from the total number on the server (regardless of how many you fetched).
*/
const browserRelationsCount = relationsFromModifiedData.length;
const serverRelationsCount = (get(initialData, name) ?? []).length;
const realServerRelationsCount = relations.data?.pages[0]?.pagination?.total ?? 0;
/**
* _IF_ theres no relations data and the browserCount is the same as serverCount you can therefore assume
* that the browser count is correct because we've just _made_ this entry and the in-component hook is now fetching.
*/
const totalRelations =
!relations.data && browserRelationsCount === serverRelationsCount
? browserRelationsCount
: browserRelationsCount - serverRelationsCount + realServerRelationsCount;
return (
<RelationInput
error={error}
@ -147,7 +169,7 @@ export const RelationInputDataManager = ({
label={`${formatMessage({
id: intlLabel.id,
defaultMessage: intlLabel.defaultMessage,
})} ${initialData[name]?.count !== undefined ? `(${initialData[name].count})` : ''}`}
})} ${totalRelations > 0 ? `(${totalRelations})` : ''}`}
labelAction={labelAction}
labelLoadMore={
!isCreatingEntry

View File

@ -101,43 +101,44 @@ jest.mock('@strapi/helper-plugin', () => ({
}),
}));
const setup = (props) =>
render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en">
<RelationInputDataManager
description="Description"
intlLabel={{
id: 'label',
defaultMessage: 'Label',
}}
labelAction={<>Action</>}
mainField={{
name: 'relation',
schema: {
type: 'relation',
},
}}
name="relation"
placeholder={{
id: 'placeholder',
defaultMessage: 'Placeholder',
}}
relationType="oneToOne"
size={6}
targetModel="something"
queryInfos={{
shouldDisplayRelationLink: true,
}}
{...props}
/>
</IntlProvider>
</ThemeProvider>
</QueryClientProvider>
</MemoryRouter>
);
const RelationInputDataManagerComponent = (props) => (
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en">
<RelationInputDataManager
description="Description"
intlLabel={{
id: 'label',
defaultMessage: 'Label',
}}
labelAction={<>Action</>}
mainField={{
name: 'relation',
schema: {
type: 'relation',
},
}}
name="relation"
placeholder={{
id: 'placeholder',
defaultMessage: 'Placeholder',
}}
relationType="oneToOne"
size={6}
targetModel="something"
queryInfos={{
shouldDisplayRelationLink: true,
}}
{...props}
/>
</IntlProvider>
</ThemeProvider>
</QueryClientProvider>
</MemoryRouter>
);
const setup = (props) => render(<RelationInputDataManagerComponent {...props} />);
describe('RelationInputDataManager', () => {
afterEach(() => {
@ -147,7 +148,7 @@ describe('RelationInputDataManager', () => {
test('Does pass through props from the CM', async () => {
const { findByText } = setup();
expect(await findByText('Label')).toBeInTheDocument();
expect(await findByText(/Label/)).toBeInTheDocument();
expect(await findByText('Description')).toBeInTheDocument();
expect(await findByText('Action')).toBeInTheDocument();
expect(await findByText('Placeholder')).toBeInTheDocument();
@ -382,4 +383,189 @@ describe('RelationInputDataManager', () => {
})
);
});
describe('Counting relations', () => {
it('should not render a count value when there are no relations', () => {
useCMEditViewDataManager.mockImplementation(() => ({
isCreatingEntry: false,
createActionAllowedFields: ['relation'],
readActionAllowedFields: ['relation'],
updateActionAllowedFields: ['relation'],
slug: 'test',
initialData: {
relation: [],
},
modifiedData: {
relation: [],
},
}));
const { queryByText } = setup();
expect(queryByText(/\([0-9]\)/)).not.toBeInTheDocument();
});
it('should render a count value when there are relations added to the store but no relations from useRelation', () => {
useCMEditViewDataManager.mockImplementation(() => ({
isCreatingEntry: false,
createActionAllowedFields: ['relation'],
readActionAllowedFields: ['relation'],
updateActionAllowedFields: ['relation'],
slug: 'test',
initialData: {
relation: [],
},
modifiedData: {
relation: [
{
id: 1,
},
{
id: 2,
},
{
id: 3,
},
],
},
}));
const { queryByText } = setup();
expect(queryByText(/\(3\)/)).toBeInTheDocument();
});
it('should render the count value of the useRelations response when there are relations from useRelation', () => {
useRelation.mockImplementation(() => ({
relations: {
data: {
pages: [
{
pagination: {
total: 8,
},
},
],
},
hasNextPage: true,
isFetchingNextPage: false,
isLoading: false,
isSuccess: true,
status: 'success',
},
search: {
data: {},
isFetchingNextPage: false,
isLoading: false,
isSuccess: true,
status: 'success',
},
}));
useCMEditViewDataManager.mockImplementation(() => ({
isCreatingEntry: false,
createActionAllowedFields: ['relation'],
readActionAllowedFields: ['relation'],
updateActionAllowedFields: ['relation'],
slug: 'test',
initialData: {
relation: [
{
id: 1,
},
],
},
modifiedData: {
relation: [
{
id: 1,
},
],
},
}));
const { queryByText } = setup();
expect(queryByText(/\(8\)/)).toBeInTheDocument();
});
it('should correct calculate browser mutations when there are relations from useRelation', async () => {
useRelation.mockImplementation(() => ({
relations: {
data: {
pages: [
{
pagination: {
total: 8,
},
},
],
},
hasNextPage: true,
isFetchingNextPage: false,
isLoading: false,
isSuccess: true,
status: 'success',
},
search: {
data: {},
isFetchingNextPage: false,
isLoading: false,
isSuccess: true,
status: 'success',
},
}));
useCMEditViewDataManager.mockImplementation(() => ({
isCreatingEntry: false,
createActionAllowedFields: ['relation'],
readActionAllowedFields: ['relation'],
updateActionAllowedFields: ['relation'],
slug: 'test',
initialData: {
relation: [
{
id: 1,
},
],
},
modifiedData: {
relation: [
{
id: 1,
},
],
},
}));
const { queryByText, rerender } = setup();
expect(queryByText(/\(8\)/)).toBeInTheDocument();
/**
* Simulate changing the store
*/
useCMEditViewDataManager.mockImplementation(() => ({
isCreatingEntry: false,
createActionAllowedFields: ['relation'],
readActionAllowedFields: ['relation'],
updateActionAllowedFields: ['relation'],
slug: 'test',
initialData: {
relation: [
{
id: 1,
},
],
},
modifiedData: {
relation: [],
},
}));
rerender(<RelationInputDataManagerComponent />);
expect(queryByText(/\(7\)/)).toBeInTheDocument();
});
});
});