Skip to content
Merged
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
39 changes: 23 additions & 16 deletions apps/central/src/components/entity/table-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { computed, inject, reactive, useTemplateRef, watch } from 'vue';
import EntityTable from './table.vue';
import OdataLoadingMessage from '../odata-loading-message.vue';
import Pagination from '../pagination.vue';
import usePaginationQueryRef from '../../composables/pagination-query-ref';

import { apiPaths } from '../../util/request';
import { noop, reemit, reexpose } from '../../util/util';
Expand All @@ -60,23 +61,19 @@ const datasetName = inject('datasetName');
const { dataset, odataEntities, deletedEntityCount } = useRequestData();

const pageSizeOptions = [250, 500, 1000];
const { pageNumber, pageSize } = usePaginationQueryRef(pageSizeOptions);
const pagination = reactive({
page: 0,
size: pageSizeOptions[0],
page: pageNumber,
size: pageSize,
count: computed(() => (odataEntities.dataExists ? odataEntities.count : 0)),
removed: computed(() => (odataEntities.dataExists ? odataEntities.removedEntities : 0))
});

// `clear` indicates whether odataEntities should be cleared before sending the
// request. `refresh` indicates whether the request is a background refresh
// (whether the refresh button was pressed).
const fetchChunk = (clear, refresh = false) => {
// Are we fetching the first chunk of entities or the next chunk?
const first = clear || refresh;
if (first) {
pagination.page = 0;
}

/**
* @param clear indicates whether this.odata should be cleared before sending the request.
* @param page indicates which page to load
*/
const fetchChunk = (clear, page = 0) => {
const $search = props.searchTerm ? props.searchTerm : undefined;

emit('clear-selection');
Expand All @@ -87,7 +84,7 @@ const fetchChunk = (clear, refresh = false) => {
datasetName,
{
$top: pagination.size,
$skip: pagination.page * pagination.size,
$skip: page * pagination.size,
$count: true,
$filter: props.deleted ? '__system/deletedAt ne null' : props.filter,
$search,
Expand All @@ -97,6 +94,14 @@ const fetchChunk = (clear, refresh = false) => {
clear
})
.then(() => {
if (pagination.page !== page) {
pagination.page = page;
}
const lastPage = Math.max(0, Math.ceil(odataEntities.count / pagination.size) - 1);
if (pagination.page > lastPage) {
fetchChunk(true, lastPage);
return;
}
if (props.deleted) {
deletedEntityCount.cancelRequest();
if (!deletedEntityCount.dataExists) {
Expand All @@ -107,19 +112,21 @@ const fetchChunk = (clear, refresh = false) => {
})
.catch(noop);
};
fetchChunk(true);

fetchChunk(true, pagination.page);

watch([() => props.deleted, () => props.filter, () => props.searchTerm], () => {
fetchChunk(true);
});
const handlePageChange = () => {
// This function is called for size change as well. So when the total number of entities are
// less than the lowest size option, hence we don't need to make a request.
if (odataEntities.count < pageSizeOptions[0]) return;
fetchChunk(false);
fetchChunk(false, pagination.page);
};
// For defineExpose()
const exposedFetch = {
fetchData: (clear = true) => fetchChunk(clear, !clear),
fetchData: (clear = true) => fetchChunk(clear),
cancelFetch: () => { odataEntities.cancelRequest(); }
};

Expand Down
44 changes: 3 additions & 41 deletions apps/central/src/components/odata-loading-message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ const message = computed(() => {
if (props.top == null || props.totalCount <= props.top)
return tn(`${props.type}.all`, props.totalCount);

return tn(`${props.type}.first`, props.totalCount, {
top: props.top
});
return t(`${props.type}.withoutCount`);
});
</script>

Expand All @@ -80,54 +78,18 @@ const message = computed(() => {
// This text is shown when the number of Submissions loading is unknown.
"withoutCount": "Loading Submissions…",
"all": "Loading {count} Submission… | Loading {count} Submissions…",
// {top} is a number that is either 250 or 1000. {count} may be any number
// that is at least 250. The string will be pluralized based on {count}.
"first": "Loading the first {top} of {count} Submission… | Loading the first {top} of {count} Submissions…",
// {top} is a number that is either 250 or 1000. {count} may be any number
// that is at least 250. The string will be pluralized based on {count}.
"middle": "Loading {top} more of {count} remaining Submission… | Loading {top} more of {count} remaining Submissions…",
"last": {
"multiple": "Loading the last {count} Submission… | Loading the last {count} Submissions…",
"one": "Loading the last Submission…"
},
"filtered": {
// This text is shown when the number of Submissions loading is unknown.
"withoutCount": "Loading matching Submissions…",
// {top} is a number that is either 250 or 1000. {count} may be any
// number that is at least 250. The string will be pluralized based on
// {count}.
"middle": "Loading {top} more of {count} remaining matching Submission… | Loading {top} more of {count} remaining matching Submissions…",
"last": {
"multiple": "Loading the last {count} matching Submission… | Loading the last {count} matching Submissions…",
"one": "Loading the last matching Submission…"
}
"withoutCount": "Loading matching Submissions…"
}
},
"entity": {
// This text is shown when the number of Entities loading is unknown.
"withoutCount": "Loading Entities…",
"all": "Loading {count} Entity… | Loading {count} Entities…",
// {top} is a number that is either 250 or 1000. {count} may be any number
// that is at least 250. The string will be pluralized based on {count}.
"first": "Loading the first {top} of {count} Entity… | Loading the first {top} of {count} Entities…",
// {top} is a number that is either 250 or 1000. {count} may be any number
// that is at least 250. The string will be pluralized based on {count}.
"middle": "Loading {top} more of {count} remaining Entity… | Loading {top} more of {count} remaining Entities…",
"last": {
"multiple": "Loading the last {count} Entity… | Loading the last {count} Entities…",
"one": "Loading the last Entity…"
},
"filtered": {
// This text is shown when the number of Entities loading is unknown.
"withoutCount": "Loading matching Entities…",
// {top} is a number that is either 250 or 1000. {count} may be any
// number that is at least 250. The string will be pluralized based on
// {count}.
"middle": "Loading {top} more of {count} remaining matching Entity… | Loading {top} more of {count} remaining matching Entities…",
"last": {
"multiple": "Loading the last {count} matching Entity… | Loading the last {count} matching Entities…",
"one": "Loading the last matching Entity…"
}
"withoutCount": "Loading matching Entities…"
}
}
}
Expand Down
97 changes: 53 additions & 44 deletions apps/central/src/components/submission/table-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ except according to the terms contained in the LICENSE file.
type="submission"
:top="pagination.size"
:filter="!!filter"
:total-count="totalCount"/>
:total-count="pagination.page ? 0 : totalCount"/>
<!-- @update:page is emitted on size change as well -->
<Pagination v-if="pagination.count > 0"
v-model:page="pagination.page" v-model:size="pagination.size"
Expand All @@ -34,6 +34,7 @@ import { computed, reactive, useTemplateRef, watch } from 'vue';
import OdataLoadingMessage from '../odata-loading-message.vue';
import Pagination from '../pagination.vue';
import SubmissionTable from './table.vue';
import usePaginationQueryRef from '../../composables/pagination-query-ref';

import { apiPaths } from '../../util/request';
import { noop, reemit, reexpose } from '../../util/util';
Expand Down Expand Up @@ -73,9 +74,10 @@ const emit = defineEmits(['review', 'delete', 'restore']);
const { odata, deletedSubmissionCount } = useRequestData();

const pageSizeOptions = [250, 500, 1000];
const { pageNumber, pageSize } = usePaginationQueryRef(pageSizeOptions);
const pagination = reactive({
page: 0,
size: pageSizeOptions[0],
page: pageNumber,
size: pageSize,
count: computed(() => (odata.dataExists ? odata.count : 0)),
removed: computed(() => (odata.dataExists ? odata.removedSubmissions : 0))
});
Expand All @@ -87,58 +89,65 @@ const odataSelect = computed(() => {
return paths.join(',');
});

// `clear` indicates whether this.odata should be cleared before sending the
// request. `refresh` indicates whether the request is a background refresh
// (whether the refresh button was pressed).
const fetchChunk = (clear, refresh = false) => {
// Are we fetching the first chunk of submissions or the next chunk?
const first = clear || refresh;
if (first) {
pagination.page = 0;
}

return odata.request({
url: apiPaths.odataSubmissions(
props.projectId,
props.xmlFormId,
props.draft,
{
$top: pagination.size,
$skip: pagination.page * pagination.size,
$count: true,
$wkt: true,
$filter: props.deleted ? '__system/deletedAt ne null' : props.filter,
$select: odataSelect.value,
$orderby: '__system/submissionDate desc'
/**
* @param clear indicates whether this.odata should be cleared before sending the request.
* @param page indicates which page to load
*/
const fetchChunk = (clear, page = 0) => odata.request({
url: apiPaths.odataSubmissions(
props.projectId,
props.xmlFormId,
props.draft,
{
$top: pagination.size,
$skip: page * pagination.size,
$count: true,
$wkt: true,
$filter: props.deleted ? '__system/deletedAt ne null' : props.filter,
$select: odataSelect.value,
$orderby: '__system/submissionDate desc'
}
),
clear
})
.then(() => {
if (pagination.page !== page) {
pagination.page = page;
}
const lastPage = Math.max(0, Math.ceil(odata.count / pagination.size) - 1);
if (pagination.page > lastPage) {
fetchChunk(true, lastPage);
return;
}
if (props.deleted) {
deletedSubmissionCount.cancelRequest();
if (!deletedSubmissionCount.dataExists) {
deletedSubmissionCount.data = reactive({});
}
),
clear
deletedSubmissionCount.value = odata.count;
}
})
.then(() => {
if (props.deleted) {
deletedSubmissionCount.cancelRequest();
if (!deletedSubmissionCount.dataExists) {
deletedSubmissionCount.data = reactive({});
}
deletedSubmissionCount.value = odata.count;
}
})
.catch(noop);
};
fetchChunk(true);
watch([() => props.filter, () => props.deleted], () => { fetchChunk(true); });
.catch(noop);

fetchChunk(true, pagination.page);

watch([() => props.filter, () => props.deleted], () => {
fetchChunk(true);
});
watch(() => props.fields, (_, oldFields) => {
// SubmissionList resets column selector when delete button is pressed, in
// that case we don't want to send request from here.
if (oldFields != null && !props.deleted) fetchChunk(true);
if (oldFields != null && !props.deleted) {
fetchChunk(true);
}
});
const handlePageChange = () => {
// This function is called for size change as well. So the total number of submissions are
// less than the lowest size option, hence we don't need to make a request.
if (odata.count < pageSizeOptions[0]) return;
fetchChunk(false);
fetchChunk(false, pagination.page);
};
const refresh = () => fetchChunk(false, true);
const refresh = () => fetchChunk(false);
const cancelRefresh = () => { odata.cancelRequest(); };

const reemitters = reemit(emit, ['review', 'delete', 'restore']);
Expand Down
11 changes: 10 additions & 1 deletion apps/central/src/composables/data-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ export default () => {

const dataView = useQueryRef({
fromQuery: (query) => (!query.deleted && query.map === 'true' ? 'map' : 'table'),
toQuery: (value) => ({ map: value === 'map' ? 'true' : null })
toQuery: (value) => {
if (value === 'map') {
return {
map: true,
'page-size': null,
'page-number': null
};
}
return { map: null };
}
});

const options = computed(() => [
Expand Down
33 changes: 33 additions & 0 deletions apps/central/src/composables/pagination-query-ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import useQueryRef from './query-ref';

/**
* @param {number[]} pageSizeOptions - An array of exactly three numbers representing
* the available page size options in ascending order (e.g., [250, 500, 1000]).
*/
export default (pageSizeOptions) => {
const pageNumber = useQueryRef({
fromQuery: (query) => {
const num = Number.parseInt(query['page-number'], 10);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I using page-number for the readability sake, we can also go with just page.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if (!num || num < 1) return 0;
return num - 1;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess page number starting from 1 is more acceptable than 0.

},
toQuery: (value) => ({ 'page-number': value === 0 ? null : value + 1 })
});

const pageSize = useQueryRef({
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expecting absolute value for page sizes like 250, 500, 1000 instead of index of options here.

fromQuery: (query) => {
const size = Number.parseInt(query['page-size'], 10);

if (!size || size < pageSizeOptions[0]) return pageSizeOptions[0];
if (size >= pageSizeOptions[2]) return pageSizeOptions[2];
if (size >= pageSizeOptions[1]) return pageSizeOptions[1];
return pageSizeOptions[0];
},
toQuery: (value) => ({ 'page-size': value })
});

return {
pageNumber,
pageSize
};
};
Loading