Add pagination info in the query parameter#1417
Add pagination info in the query parameter#1417sadiqkhoja wants to merge 1 commit intogetodk:masterfrom
Conversation
|
A few quick thoughts from me!
I think this PR will conflict with getodk/central#1434, just to the extent that I'll be adding an
If there are pagination-related query parameters, do those currently persist when toggling to map view? For example:
How about we add a composable for the pagination-related query parameters? I'm thinking of something similar to |
| const lastPage = Math.max(0, Math.ceil(pagination.count / pagination.size) - 1); | ||
| if (pagination.page > lastPage) { | ||
| pagination.page = lastPage; | ||
| fetchChunk(false); |
There was a problem hiding this comment.
Are you specifying false so that the snapshot filter isn't reset?
One thing I'm thinking about is that it'd be good to show a loading message during this second fetchChunk(). Otherwise, if that request takes a while, the user might get confused and think that lastPage is empty. Maybe we could show the loading message by setting odata.data to null here before calling fetchChunk(). Or maybe that logic belongs in fetchChunk(), and fetchChunk() just needs different options than clear and refresh. 🤔
There was a problem hiding this comment.
introduced skipSettingSnapshotFilter in fetchChunk
There was a problem hiding this comment.
removed skipSettingSnapshotFilter as we are no longer using snapshot filters.
and set fetchChunk(true) so that loading message is shown
src/components/entity/list.vue
Outdated
| if (!num || num < 0) return 0; | ||
| return num - 1; | ||
| }, | ||
| toQuery: (value) => ({ 'page-number': value + 1 }) |
There was a problem hiding this comment.
What do you think about removing the query parameter for the first page? Since the first page is the default — one less query parameter in the URL.
| toQuery: (value) => ({ 'page-number': value + 1 }) | |
| toQuery: (value) => ({ 'page-number': value === 0 ? null : value + 1 }) |
|
|
||
| describe('SubmissionFieldDropdown', () => { | ||
| beforeEach(() => { | ||
| mockLogin(); |
There was a problem hiding this comment.
Why is mockLogin() needed here out of curiosity? Something to do with the router?
There was a problem hiding this comment.
because I changed the router from mock to test in test/util/submission.js, with that change request to sessions is made causing the tests to fail.
Another thing I can do is to add replace method to the list of supported method for mockRouter.
There was a problem hiding this comment.
Another thing I can do is to add
replacemethod to the list of supported method for mockRouter.
I'm open to this. It sure would be nice for there to be fewer cases in which testRouter() is needed. As-is, the difference between mockRouter() and testRouter() can feel sort of complicated.
Part of my idea for mockRouter() was that it isolates the individual component being tested from all the logic in the router (the beforeEach guards, etc.). To keep with that theme, maybe replace() could look something like this in mockRouter():
replace: (location) => {
currentRoute.value = router.resolve(location);
},In other words, it's not actually doing a navigation (it's not actually calling router.replace()). Instead, it's just setting currentRoute as if the navigation took place. mockRouter() would no longer be read-only, but it would still be different from testRouter() in that it would never run navigation hooks.
Some other things would have to change in mockRouter() though, like the way that $route is provided on app install. It's just a constant right now.
This path sounds promising, but also no need to go down it right now if it doesn't feel like the right time. I'm happy with us using mockLogin() here.
8081b98 to
0bdd1e6
Compare
src/components/entity/list.vue
Outdated
| :top="pagination.size" | ||
| :filter="odataFilter != null || !!searchTerm" | ||
| :total-count="dataset.dataExists ? dataset.entities : 0"/> | ||
| :total-count="dataset.dataExists && !pagination.page ? dataset.entities : 0"/> |
There was a problem hiding this comment.
I don't want to show "Loading first N entities...", with this change it will always display "Loading entiities..." when there is a pagination information in the query parameter on the first loading. Note that on change pages using pagination control doesn't show any loading message; we show spinner in the pagination control.
There was a problem hiding this comment.
Could you say more about why you don't want to show "Loading first N entities…"? I'm OK with that change, just wondering whether Nicole suggested it or if there was another reason.
Note that on change pages using pagination control doesn't show any loading message
If that's the case, I sort of think we should simplify OdataLoadingMessage. It has so many cases, many of which aren't about the first page. If some of its i18n messages aren't useful anymore, it'd be nice to remove them so that translators don't have to translate them.
One option is that we make that change in a follow-up PR. In that case, maybe you could file a refactor issue about it?
| fromQuery: (query) => { | ||
| const num = Number.parseInt(query['page-number'], 10); | ||
| if (!num || num < 0) return 0; | ||
| return num - 1; |
There was a problem hiding this comment.
should I update the query parameter here? if the given value is out of valid range? we don't do that for invalid values of filters, right?
There was a problem hiding this comment.
we don't do that for invalid values of filters, right?
That's right. If filter query parameters are supplied invalid values, those values are generally ignored. The URL isn't changed/corrected, but nothing explodes either. The invalid values fall back to something reasonable.
In some cases, we can't know right away whether a value is valid. For example, specifying a submitter ID that doesn't exist: we can't know that it's wrong until we receive the response for the list of submitters (async).
should I update the query parameter here?
I'm open to it, but it feels more complex to me. It seems simpler not to change the route if we don't need to. It should be pretty rare that someone specifies an invalid value.
if the given value is out of valid range?
"Out of valid range" just refers to if (!num || num < 0) return 0; right? That logic sounds great to me.
|
|
||
| describe('SubmissionFieldDropdown', () => { | ||
| beforeEach(() => { | ||
| mockLogin(); |
There was a problem hiding this comment.
because I changed the router from mock to test in test/util/submission.js, with that change request to sessions is made causing the tests to fail.
Another thing I can do is to add replace method to the list of supported method for mockRouter.
I am okay with either way, I can hold this PR till maps for Entities are done or at list
Fixed it
Created |
| export default (pageSizeOptions = [250, 500, 1000]) => { | ||
| const pageNumber = useQueryRef({ | ||
| fromQuery: (query) => { | ||
| const num = Number.parseInt(query['page-number'], 10); |
There was a problem hiding this comment.
I using page-number for the readability sake, we can also go with just page.
There was a problem hiding this comment.
I definitely prefer page over page-number. It's much shorter, and I see it on many sites. page + page-size sounds like a nice combo to me. I don't have the strongest feelings about it though, we could run with page-number for now and see if anyone else on the team voices an opinion.
| fromQuery: (query) => { | ||
| const num = Number.parseInt(query['page-number'], 10); | ||
| if (!num || num < 0) return 0; | ||
| return num - 1; |
There was a problem hiding this comment.
I guess page number starting from 1 is more acceptable than 0.
| toQuery: (value) => ({ 'page-number': value === 0 ? null : value + 1 }) | ||
| }); | ||
|
|
||
| const pageSize = useQueryRef({ |
There was a problem hiding this comment.
Expecting absolute value for page sizes like 250, 500, 1000 instead of index of options here.
0bdd1e6 to
66eb211
Compare
66eb211 to
91183bc
Compare
PR Feedback + Created paginationQueryRef composable + Removed pagination query parameter when navigating to the map view + Included skipSettingSnapshotFilter to the fetchChunk function
91183bc to
116a34d
Compare
|
@matthew-white this is ready for re-review. |
matthew-white
left a comment
There was a problem hiding this comment.
It's looking good!
Probably the main question I still have is where/when pagination.page is set to 0.
In some cases, I made non-essential suggestions. I know we want to get this PR merged soon, so I'm happy with us skipping some of those suggestions or filing follow-up issues about them.
| const first = clear || refresh; | ||
| if (first) { | ||
| if (refresh) { | ||
| pagination.page = 0; |
There was a problem hiding this comment.
I'm trying to think through the ramifications of pagination.page = 0; for a background refresh. Wouldn't that cause the pagination control to immediately change to the first page? But the actual table wouldn't change until the request completes. If that takes a while, I think it'd be confusing for the pagination control to say that it's on the first page. Or even worse, if the request results in an error, the pagination control would incorrectly think that it's on the first page.
If it's a background refresh, I feel like pagination.page should be set to 0 only once the response is received.
It kind of feels like that would be changing the current behavior of background refresh though. Right now, does it refresh the current page rather than returning to the first page? If so, I feel like changing that behavior should maybe go in its own PR. Then this PR could just be focused on query parameters.
| :count="pagination.count" :size-options="pageSizeOptions" | ||
| :spinner="odata.awaitingResponse" | ||
| :removed="pagination.removed" | ||
| @update:page="handlePageChange"/> |
There was a problem hiding this comment.
Instead of calling handlePageChange this way by listening for an event, I'm wondering whether we should add a watcher on pagination.page. Now there are multiple pathways that can change pagination.page. If the user changes the page number in the URL, that will change pagination.page, and I'd expect that to have an effect on the page itself. If that's hard to do, it's probably not a big deal, but that's how filters work right now. Hopefully just making it a watcher would work out.
| fetchChunk(true); | ||
| watch([() => props.filter, () => props.deleted], () => { fetchChunk(true); }); | ||
| watch([() => props.filter, () => props.deleted], () => { | ||
| pagination.page = 0; |
There was a problem hiding this comment.
Right now, if you change the selection of columns / form fields, you're sent back to the first page. But I'm only seeing this line pagination.page = 0; for filter changes, not a change to the column selection. I could be convinced that the current behavior isn't important and is fine to change, but I'm wondering if there's a different way to approach this. I kind of like how fetchChunk() used to be responsible for setting pagination.page in more cases. What if fetchChunk() took a page number argument that defaulted to 0? That would also work for the case where the user navigates past the last page. Like I said, I'm open to various approaches here, I just wanted to throw out these thoughts.
| /* | ||
| Copyright 2025 ODK Central Developers | ||
| See the NOTICE file at the top-level directory of this distribution and at | ||
| https://github.com/getodk/central-frontend/blob/master/NOTICE. | ||
|
|
||
| This file is part of ODK Central. It is subject to the license terms in | ||
| the LICENSE file found in the top-level directory of this distribution and at | ||
| https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, | ||
| including this file, may be copied, modified, propagated, or distributed | ||
| except according to the terms contained in the LICENSE file. | ||
| */ |
There was a problem hiding this comment.
We don't need these file headers for new files anymore. 🥳 So this can be removed.
|
|
||
| import useQueryRef from './query-ref'; | ||
|
|
||
| export default (pageSizeOptions = [250, 500, 1000]) => { |
There was a problem hiding this comment.
I'm not sure we need a default value here. It's hard for me to think of a case where pageSizeOptions wouldn't be provided. sizeOptions is a required prop in the Pagination component, so it should always exist. If we were to add the concept of a default set of size options, that should probably go in multiple places (e.g., Pagination as well).
| }); | ||
| }); | ||
|
|
||
| it('floors page-size to nearest valid value when invalid page-size is provided in URL', () => { |
There was a problem hiding this comment.
For some of these edge cases, I feel like they'd be better tested in unit tests of the usePaginationQueryRef() composable rather than in every component that uses the composable. There are already so many tests of the EntityList component.
withSetup() provides a way to unit-test a composable. We have unit tests of useQueryRef() that you might find interesting to look at.
Given that these tests are already written, I'm happy to run with them as-is. Mostly I just wanted to share the thought that I think there can be benefit to unit-testing composables and that withSetup() provides a way to do so.
| router: mockRouter(form.publishedAt != null | ||
| ? `/projects/${project.id}/forms/${encodeURIComponent(form.xmlFormId)}/submissions` | ||
| : `/projects/${project.id}/forms/${encodeURIComponent(form.xmlFormId)}/draft`) |
There was a problem hiding this comment.
That's interesting that it's OK for these paths to go away. I see above that loadEntityList() has never specified a path to mockRouter(). There would probably be options to specify a path if we needed to. Maybe by calling .route(), similar to what's done in test/components/submission/filters.spec.js. There, loadSubmissionList() is called with testRouter(), then .route() is called so that the initial route is set correctly. If it's working now though, I guess nothing needs to change 🤷 — no path is needed here. But maybe let's at least get rid of testRouter() in submission/filters.spec.js if testRouter() is going to be the default going forward.
Closes getodk/central#1348
What has been done to verify that this works as intended?
Manual verification + added few tests.
Why is this the best possible solution? Were any other approaches considered?
Please see inline comments.
How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
No
Does this change require updates to user documentation? If so, please file an issue here and include the link below.
No
Before submitting this PR, please make sure you have:
npm run testandnpm run lintand confirmed all checks still pass OR confirm CircleCI build passes