Skip to content

Commit 22f1f40

Browse files
authored
Merge pull request #11022 from marmelab/better-list-url-management
Improve lists URL management
2 parents 3769674 + cdba26c commit 22f1f40

2 files changed

Lines changed: 337 additions & 11 deletions

File tree

packages/ra-core/src/controller/list/useListParams.spec.tsx

Lines changed: 271 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ import { CoreAdminContext } from '../../core';
66

77
import { testDataProvider } from '../../dataProvider';
88
import { useStore } from '../../store/useStore';
9-
import { useListParams, getQuery, getNumberOrDefault } from './useListParams';
9+
import {
10+
useListParams,
11+
getQuery,
12+
getNumberOrDefault,
13+
ListParamsOptions,
14+
} from './useListParams';
1015
import { SORT_DESC, SORT_ASC } from './queryReducer';
1116
import { TestMemoryRouter } from '../../routing';
17+
import { memoryStore } from '../../store';
1218

1319
describe('useListParams', () => {
1420
describe('getQuery', () => {
@@ -360,11 +366,16 @@ describe('useListParams', () => {
360366
});
361367
});
362368
describe('useListParams', () => {
363-
const Component = ({ disableSyncWithLocation = false }) => {
364-
const [{ page }, { setPage }] = useListParams({
365-
resource: 'posts',
366-
disableSyncWithLocation,
367-
});
369+
const Component = ({
370+
disableSyncWithLocation = false,
371+
...options
372+
}: Partial<ListParamsOptions>) => {
373+
const [{ page, perPage, sort, order, filter }, { setPage }] =
374+
useListParams({
375+
resource: 'posts',
376+
disableSyncWithLocation,
377+
...options,
378+
});
368379

369380
const handleClick = () => {
370381
setPage(10);
@@ -373,6 +384,10 @@ describe('useListParams', () => {
373384
return (
374385
<>
375386
<p>page: {page}</p>
387+
<p>perPage: {perPage}</p>
388+
<p>sort: {sort}</p>
389+
<p>order: {order}</p>
390+
<p>filter: {JSON.stringify(filter)}</p>
376391
<button onClick={handleClick}>update</button>
377392
</>
378393
);
@@ -495,6 +510,71 @@ describe('useListParams', () => {
495510
});
496511
});
497512

513+
it('should synchronize location with store when sync is enabled', async () => {
514+
let location;
515+
let storeValue;
516+
const StoreReader = () => {
517+
const [value] = useStore('posts.listParams');
518+
React.useEffect(() => {
519+
storeValue = value;
520+
}, [value]);
521+
return null;
522+
};
523+
render(
524+
<TestMemoryRouter
525+
locationCallback={l => {
526+
location = l;
527+
}}
528+
>
529+
<CoreAdminContext
530+
dataProvider={testDataProvider()}
531+
store={memoryStore({
532+
'posts.listParams': {
533+
sort: 'id',
534+
order: 'ASC',
535+
page: 10,
536+
perPage: 10,
537+
filter: {},
538+
},
539+
})}
540+
>
541+
<Component />
542+
<StoreReader />
543+
</CoreAdminContext>
544+
</TestMemoryRouter>
545+
);
546+
547+
await waitFor(() => {
548+
expect(storeValue).toEqual({
549+
sort: 'id',
550+
order: 'ASC',
551+
page: 10,
552+
perPage: 10,
553+
filter: {},
554+
});
555+
});
556+
557+
await waitFor(() => {
558+
expect(location).toEqual(
559+
expect.objectContaining({
560+
hash: '',
561+
key: expect.any(String),
562+
state: null,
563+
pathname: '/',
564+
search:
565+
'?' +
566+
stringify({
567+
filter: JSON.stringify({}),
568+
sort: 'id',
569+
order: 'ASC',
570+
page: 10,
571+
perPage: 10,
572+
}),
573+
})
574+
);
575+
});
576+
});
577+
498578
it('should not synchronize parameters with location and store when sync is not enabled', async () => {
499579
let location;
500580
let storeValue;
@@ -540,6 +620,191 @@ describe('useListParams', () => {
540620
expect(storeValue).toBeUndefined();
541621
});
542622

623+
it('should not synchronize location with store if the location already contains parameters', async () => {
624+
let location;
625+
render(
626+
<TestMemoryRouter
627+
initialEntries={[
628+
{
629+
search:
630+
'?' +
631+
stringify({
632+
filter: JSON.stringify({}),
633+
sort: 'id',
634+
order: 'ASC',
635+
page: 5,
636+
perPage: 10,
637+
}),
638+
},
639+
]}
640+
locationCallback={l => {
641+
location = l;
642+
}}
643+
>
644+
<CoreAdminContext
645+
dataProvider={testDataProvider()}
646+
store={memoryStore({
647+
'posts.listParams': {
648+
sort: 'id',
649+
order: 'ASC',
650+
page: 10,
651+
perPage: 10,
652+
filter: {},
653+
},
654+
})}
655+
>
656+
<Component />
657+
</CoreAdminContext>
658+
</TestMemoryRouter>
659+
);
660+
661+
await waitFor(() => {
662+
expect(location).toEqual(
663+
expect.objectContaining({
664+
hash: '',
665+
key: expect.any(String),
666+
state: null,
667+
pathname: '/',
668+
search:
669+
'?' +
670+
stringify({
671+
filter: JSON.stringify({}),
672+
sort: 'id',
673+
order: 'ASC',
674+
page: 5,
675+
perPage: 10,
676+
}),
677+
})
678+
);
679+
});
680+
});
681+
682+
it('should not synchronize location with store if the store parameters are the defaults', async () => {
683+
let location;
684+
render(
685+
<TestMemoryRouter
686+
locationCallback={l => {
687+
location = l;
688+
}}
689+
>
690+
<CoreAdminContext dataProvider={testDataProvider()}>
691+
<Component />
692+
</CoreAdminContext>
693+
</TestMemoryRouter>
694+
);
695+
696+
// Let React do its thing
697+
await new Promise(resolve => setTimeout(resolve, 0));
698+
699+
await waitFor(() => {
700+
expect(location).toEqual(
701+
expect.objectContaining({
702+
hash: '',
703+
key: expect.any(String),
704+
state: null,
705+
pathname: '/',
706+
search: '',
707+
})
708+
);
709+
});
710+
});
711+
712+
it('should not synchronize location with store if the store parameters are the custom defaults provided to the hook', async () => {
713+
let location;
714+
render(
715+
<TestMemoryRouter
716+
locationCallback={l => {
717+
location = l;
718+
}}
719+
>
720+
<CoreAdminContext dataProvider={testDataProvider()}>
721+
<Component
722+
perPage={5}
723+
sort={{ field: 'title', order: 'DESC' }}
724+
/>
725+
</CoreAdminContext>
726+
</TestMemoryRouter>
727+
);
728+
729+
// Let React do its thing
730+
await new Promise(resolve => setTimeout(resolve, 0));
731+
732+
// The list is using the default set on the component
733+
await screen.findByText('perPage: 5');
734+
await screen.findByText('sort: title');
735+
await screen.findByText('order: DESC');
736+
737+
// The location is the default for the list (no query parameters)
738+
await waitFor(() => {
739+
expect(location).toEqual(
740+
expect.objectContaining({
741+
hash: '',
742+
key: expect.any(String),
743+
state: null,
744+
pathname: '/',
745+
search: '',
746+
})
747+
);
748+
});
749+
});
750+
751+
it('should not synchronize location with store when sync is not enabled', async () => {
752+
let location;
753+
let storeValue;
754+
const StoreReader = () => {
755+
const [value] = useStore('posts.listParams');
756+
React.useEffect(() => {
757+
storeValue = value;
758+
}, [value]);
759+
return null;
760+
};
761+
render(
762+
<TestMemoryRouter
763+
locationCallback={l => {
764+
location = l;
765+
}}
766+
>
767+
<CoreAdminContext
768+
dataProvider={testDataProvider()}
769+
store={memoryStore({
770+
'posts.listParams': {
771+
sort: 'id',
772+
order: 'ASC',
773+
page: 10,
774+
perPage: 10,
775+
filter: {},
776+
},
777+
})}
778+
>
779+
<Component disableSyncWithLocation />
780+
<StoreReader />
781+
</CoreAdminContext>
782+
</TestMemoryRouter>
783+
);
784+
785+
await waitFor(() => {
786+
expect(storeValue).toEqual({
787+
sort: 'id',
788+
order: 'ASC',
789+
page: 10,
790+
perPage: 10,
791+
filter: {},
792+
});
793+
});
794+
795+
await waitFor(() => {
796+
expect(location).toEqual(
797+
expect.objectContaining({
798+
hash: '',
799+
key: expect.any(String),
800+
state: null,
801+
pathname: '/',
802+
search: '',
803+
})
804+
);
805+
});
806+
});
807+
543808
it('should synchronize parameters with store when sync is not enabled and storeKey is passed', async () => {
544809
let storeValue;
545810
const Component = ({

0 commit comments

Comments
 (0)