Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions packages/ra-core/src/controller/list/useListParams.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useStore } from '../../store/useStore';
import { useListParams, getQuery, getNumberOrDefault } from './useListParams';
import { SORT_DESC, SORT_ASC } from './queryReducer';
import { TestMemoryRouter } from '../../routing';
import { memoryStore } from '../../store';

describe('useListParams', () => {
describe('getQuery', () => {
Expand Down Expand Up @@ -495,6 +496,71 @@ describe('useListParams', () => {
});
});

it('should synchronize location with store when sync is enabled', async () => {
let location;
let storeValue;
const StoreReader = () => {
const [value] = useStore('posts.listParams');
React.useEffect(() => {
storeValue = value;
}, [value]);
return null;
};
render(
<TestMemoryRouter
locationCallback={l => {
location = l;
}}
>
<CoreAdminContext
dataProvider={testDataProvider()}
store={memoryStore({
'posts.listParams': {
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
filter: {},
},
})}
>
<Component />
<StoreReader />
</CoreAdminContext>
</TestMemoryRouter>
);

await waitFor(() => {
expect(storeValue).toEqual({
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
filter: {},
});
});

await waitFor(() => {
expect(location).toEqual(
expect.objectContaining({
hash: '',
key: expect.any(String),
state: null,
pathname: '/',
search:
'?' +
stringify({
filter: JSON.stringify({}),
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
}),
})
);
});
});

it('should not synchronize parameters with location and store when sync is not enabled', async () => {
let location;
let storeValue;
Expand Down Expand Up @@ -540,6 +606,171 @@ describe('useListParams', () => {
expect(storeValue).toBeUndefined();
});

it('should not synchronize location with store if the location already contains parameters', async () => {
let location;
let storeValue;
const StoreReader = () => {
const [value] = useStore('posts.listParams');
React.useEffect(() => {
storeValue = value;
}, [value]);
return null;
};
render(
<TestMemoryRouter
initialEntries={[
{
search:
'?' +
stringify({
filter: JSON.stringify({}),
sort: 'id',
order: 'ASC',
page: 5,
perPage: 10,
}),
},
]}
locationCallback={l => {
location = l;
}}
>
<CoreAdminContext
dataProvider={testDataProvider()}
store={memoryStore({
'posts.listParams': {
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
filter: {},
},
})}
>
<Component disableSyncWithLocation />
<StoreReader />
</CoreAdminContext>
</TestMemoryRouter>
);

await waitFor(() => {
expect(storeValue).toEqual({
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
filter: {},
});
});

await waitFor(() => {
expect(location).toEqual(
expect.objectContaining({
hash: '',
key: expect.any(String),
state: null,
pathname: '/',
search:
'?' +
stringify({
filter: JSON.stringify({}),
sort: 'id',
order: 'ASC',
page: 5,
perPage: 10,
}),
})
);
});
});

it('should not synchronize location with store if the store parameters are the defaults', async () => {
let location;
render(
<TestMemoryRouter
locationCallback={l => {
location = l;
}}
>
<CoreAdminContext dataProvider={testDataProvider()}>
<Component disableSyncWithLocation />
</CoreAdminContext>
</TestMemoryRouter>
);

// Let React do its thing
await new Promise(resolve => setTimeout(resolve, 0));

await waitFor(() => {
expect(location).toEqual(
expect.objectContaining({
hash: '',
key: expect.any(String),
state: null,
pathname: '/',
search: '',
})
);
});
});

it('should not synchronize location with store when sync is not enabled', async () => {
let location;
let storeValue;
const StoreReader = () => {
const [value] = useStore('posts.listParams');
React.useEffect(() => {
storeValue = value;
}, [value]);
return null;
};
render(
<TestMemoryRouter
locationCallback={l => {
location = l;
}}
>
<CoreAdminContext
dataProvider={testDataProvider()}
store={memoryStore({
'posts.listParams': {
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
filter: {},
},
})}
>
<Component disableSyncWithLocation />
<StoreReader />
</CoreAdminContext>
</TestMemoryRouter>
);

await waitFor(() => {
expect(storeValue).toEqual({
sort: 'id',
order: 'ASC',
page: 10,
perPage: 10,
filter: {},
});
});

await waitFor(() => {
expect(location).toEqual(
expect.objectContaining({
hash: '',
key: expect.any(String),
state: null,
pathname: '/',
search: '',
})
);
});
});

it('should synchronize parameters with store when sync is not enabled and storeKey is passed', async () => {
let storeValue;
const Component = ({
Expand Down
57 changes: 57 additions & 0 deletions packages/ra-core/src/controller/list/useListParams.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useEffect, useState, useRef } from 'react';
import { parse, stringify } from 'query-string';
import lodashDebounce from 'lodash/debounce.js';
import isEqual from 'lodash/isEqual.js';
import { useNavigate, useLocation } from 'react-router-dom';

import { useStore } from '../../store';
Expand Down Expand Up @@ -133,6 +134,62 @@ export const useListParams = ({
}
}, [location.search]); // eslint-disable-line

const currentStoreKey = useRef(storeKey);
// if the location includes params (for example from a link like
// the categories products on the demo), we need to persist them in the
// store as well so that we don't lose them after a redirection back
// to the list
useEffect(
() => {
// If the storeKey has changed, ignore the first effect call. This avoids conflicts between lists sharing
// the same resource but different storeKeys.
if (currentStoreKey.current !== storeKey) {
// storeKey has changed
currentStoreKey.current = storeKey;
return;
}
if (disableSyncWithLocation) {
return;
}
const defaultParams = {
filter: filterDefaultValues || {},
page: 1,
perPage,
sort: sort.field,
order: sort.order,
};

if (
// The location params are not empty (we don't want to override them if provided)
Object.keys(queryFromLocation).length > 0 ||
// or the stored params are different from the location params
isEqual(query, queryFromLocation) ||
// or the stored params are not different from the default params (to keep the URL simple when possible)
isEqual(query, defaultParams)
) {
return;
}
navigate({
search: `?${stringify({
...query,
filter: JSON.stringify(query.filter),
displayedFilters: JSON.stringify(query.displayedFilters),
})}`,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
navigate,
disableSyncWithLocation,
filterDefaultValues,
perPage,
sort,
query,
location.search,
params,
]
);

const changeParams = useCallback(
action => {
// do not change params if the component is already unmounted
Expand Down
Loading