mirror of
https://github.com/strapi/strapi.git
synced 2025-12-26 14:44:31 +00:00
Merge pull request #14768 from strapi/fix/relations/re-add-count
feat: re-add the count & make it dynamic
This commit is contained in:
commit
879d051f9b
@ -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) ||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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]
|
||||
);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user