Skip to content

Commit a8ee312

Browse files
committed
Move pagination to the server side
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent a2907de commit a8ee312

6 files changed

Lines changed: 297 additions & 175 deletions

File tree

docs/API_v3.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,8 @@ Get all Submissions to a Form
653653
| Parameter | Type | Description |
654654
|-----------|---------|-------------|
655655
| _formId_ | Integer | ID of the form to get the submissions for |
656+
| _limit_ | Integer | How many items to get |
657+
| _offset_ | Integer | How many items to skip for a pagination |
656658
- Response: An Array of all submissions, sorted as newest first, as well as an array of the corresponding questions.
657659

658660
```
@@ -741,7 +743,8 @@ Get all Submissions to a Form
741743
"options": [],
742744
"extraSettings": {}
743745
}
744-
]
746+
],
747+
"totalSubmissionsCount": 40
745748
}
746749
```
747750

lib/Controller/ApiController.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,7 +1147,7 @@ public function reorderOptions(int $formId, int $questionId, array $newOrder) {
11471147
#[NoAdminRequired()]
11481148
#[BruteForceProtection(action: 'form')]
11491149
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}/submissions')]
1150-
public function getSubmissions(int $formId, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
1150+
public function getSubmissions(int $formId, ?string $query = null, int $limit = 20, int $offset = 0, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
11511151
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS);
11521152

11531153
if ($fileFormat !== null) {
@@ -1158,7 +1158,8 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
11581158
}
11591159

11601160
// Load submissions and currently active questions
1161-
$submissions = $this->submissionService->getSubmissions($formId);
1161+
$submissions = $this->submissionService->getSubmissions($formId, $query, $limit, $offset);
1162+
$totalSubmissionsCount = $this->submissionMapper->countSubmissions($formId, $query);
11621163
$questions = $this->formsService->getQuestions($formId);
11631164

11641165
// Append Display Names
@@ -1190,6 +1191,7 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
11901191
$response = [
11911192
'submissions' => $submissions,
11921193
'questions' => $questions,
1194+
'totalSubmissionsCount' => $totalSubmissionsCount,
11931195
];
11941196

11951197
return new DataResponse($response);

lib/Db/SubmissionMapper.php

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace OCA\Forms\Db;
99

10+
use OC\DB\QueryBuilder\QueryFunction;
1011
use OCP\AppFramework\Db\DoesNotExistException;
1112
use OCP\AppFramework\Db\QBMapper;
1213
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -30,19 +31,34 @@ public function __construct(
3031

3132
/**
3233
* @param int $formId
34+
* @param ?string $query
35+
* @param int $limit
36+
* @param int $offset
3337
* @throws DoesNotExistException if not found
3438
* @return Submission[]
3539
*/
36-
public function findByForm(int $formId): array {
40+
public function findByForm(int $formId, ?string $query = null, int $limit = 20, int $offset = 0): array {
3741
$qb = $this->db->getQueryBuilder();
3842

39-
$qb->select('*')
40-
->from($this->getTableName())
41-
->where(
43+
if ($query) {
44+
$qb->select('MAX(submissions.id) AS id, MAX(submissions.form_id) AS form_id, MAX(submissions.user_id) AS user_id, MAX(submissions.timestamp) AS timestamp')
45+
->join('submissions', $this->answerMapper->getTableName(), 'answers', $qb->expr()->eq('submissions.id', 'answers.submission_id'))
46+
->where($qb->expr()->like('answers.text', $qb->createNamedParameter('%' . $query . '%')))
47+
->groupBy('submissions.id');
48+
} else {
49+
$qb->select('*');
50+
}
51+
52+
$qb->from($this->getTableName(), 'submissions')
53+
->andWhere(
4254
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
4355
)
4456
//Newest submissions first
45-
->orderBy('timestamp', 'DESC');
57+
->orderBy('timestamp', 'DESC')
58+
->setMaxResults($limit)
59+
->setFirstResult($offset);
60+
61+
//var_dump($qb->getSQL());exit;
4662

4763
return $this->findEntities($qb);
4864
}
@@ -88,8 +104,8 @@ public function hasFormSubmissionsByUser(Form $form, string $userId): bool {
88104
* @param int $formId ID of the form to count submissions
89105
* @throws \Exception
90106
*/
91-
public function countSubmissions(int $formId): int {
92-
return $this->countSubmissionsWithFilters($formId, null, -1);
107+
public function countSubmissions(int $formId, ?string $searchString = null): int {
108+
return $this->countSubmissionsWithFilters($formId, null, -1, $searchString);
93109
}
94110

95111
/**
@@ -99,15 +115,25 @@ public function countSubmissions(int $formId): int {
99115
* @param int $limit allows to limit the query selection. If -1, the restriction is ignored
100116
* @throws \Exception
101117
*/
102-
protected function countSubmissionsWithFilters(int $formId, ?string $userId = null, int $limit = -1): int {
118+
protected function countSubmissionsWithFilters(int $formId, ?string $userId = null, int $limit = -1, ?string $searchString = null): int {
103119
$qb = $this->db->getQueryBuilder();
104120

105-
$query = $qb->select($qb->func()->count('*', 'num_submissions'))
106-
->from($this->getTableName())
121+
if ($searchString) {
122+
$query = $qb->select(new QueryFunction('COUNT(DISTINCT submissions.id) AS num_submissions'))
123+
->join('submissions', $this->answerMapper->getTableName(), 'answers', $qb->expr()->eq('submissions.id', 'answers.submission_id'))
124+
->andWhere($qb->expr()->like('answers.text', $qb->createNamedParameter('%' . $searchString . '%')));
125+
} else {
126+
$query = $qb->select($qb->func()->count('*', 'num_submissions'));
127+
}
128+
129+
$query
130+
->from($this->getTableName(), 'submissions')
107131
->where($qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)));
132+
108133
if (!is_null($userId)) {
109134
$query->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
110135
}
136+
111137
if ($limit !== -1) {
112138
$query->setMaxResults($limit);
113139
}

lib/Service/SubmissionService.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ private function getAnswers(int $submissionId): array {
9999
* answers: list<FormsAnswer>,
100100
* }>
101101
*/
102-
public function getSubmissions(int $formId): array {
102+
public function getSubmissions(int $formId, ?string $query = null, int $limit = 20, int $offset = 0): array {
103103
$submissionList = [];
104104
try {
105-
$submissionEntities = $this->submissionMapper->findByForm($formId);
105+
$submissionEntities = $this->submissionMapper->findByForm($formId, $query, $limit, $offset);
106106
foreach ($submissionEntities as $submissionEntity) {
107107
$submission = $submissionEntity->read();
108108
$submission['answers'] = $this->getAnswers($submission['id']);
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<div class="pagination-block">
8+
<div class="pagination-items">
9+
<NcButton
10+
type="tertiary"
11+
:disabled="totalPages === 1 || pageNumber <= 1"
12+
:aria-label="t('forms', 'Go to first page')"
13+
@click="pageNumber = 1">
14+
<template #icon>
15+
<PageFirstIcon :size="20" />
16+
</template>
17+
</NcButton>
18+
<NcButton
19+
type="tertiary"
20+
:disabled="totalPages === 1 || pageNumber <= 1"
21+
:aria-label="t('forms', 'Go to previous page')"
22+
@click="pageNumber--">
23+
<template #icon>
24+
<IconChevronLeft :size="20" />
25+
</template>
26+
</NcButton>
27+
<div class="page-number">
28+
<NcSelect
29+
v-model="pageNumber"
30+
:options="allPageNumbersArray"
31+
:aria-label-combobox="t('forms', 'Page number')">
32+
<template #selected-option-container="{ option }">
33+
<span class="selected-page">
34+
{{
35+
t('forms', '{page} of {totalPages}', {
36+
page: option.label,
37+
totalPages,
38+
})
39+
}}
40+
</span>
41+
</template>
42+
</NcSelect>
43+
</div>
44+
<NcButton
45+
type="tertiary"
46+
:disabled="totalPages === 1 || pageNumber >= totalPages"
47+
:aria-label="t('forms', 'Go to next page')"
48+
@click="pageNumber++">
49+
<template #icon>
50+
<IconChevronRight :size="20" />
51+
</template>
52+
</NcButton>
53+
<NcButton
54+
type="tertiary"
55+
:disabled="totalPages === 1 || pageNumber >= totalPages"
56+
:aria-label="t('forms', 'Go to last page')"
57+
@click="pageNumber = totalPages">
58+
<template #icon>
59+
<PageLastIcon :size="20" />
60+
</template>
61+
</NcButton>
62+
</div>
63+
</div>
64+
</template>
65+
66+
<script>
67+
import IconChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'
68+
import IconChevronRight from 'vue-material-design-icons/ChevronRight.vue'
69+
import IconMagnify from 'vue-material-design-icons/Magnify.vue'
70+
import NcButton from '@nextcloud/vue/components/NcButton'
71+
import NcSelect from '@nextcloud/vue/components/NcSelect'
72+
import NcTextField from '@nextcloud/vue/components/NcTextField'
73+
import PageFirstIcon from 'vue-material-design-icons/PageFirst.vue'
74+
import PageLastIcon from 'vue-material-design-icons/PageLast.vue'
75+
76+
export default {
77+
name: 'PaginationToolbar',
78+
emits: ['update:offset'],
79+
components: {
80+
IconChevronLeft,
81+
IconChevronRight,
82+
IconMagnify,
83+
NcButton,
84+
NcSelect,
85+
NcTextField,
86+
PageFirstIcon,
87+
PageLastIcon,
88+
},
89+
90+
props: {
91+
totalItemsCount: {
92+
type: Number,
93+
required: true,
94+
},
95+
limit: {
96+
type: Number,
97+
default: 20,
98+
},
99+
offset: {
100+
type: Number,
101+
default: 0,
102+
},
103+
},
104+
105+
computed: {
106+
allPageNumbersArray() {
107+
return Array.from(
108+
{ length: this.totalPages },
109+
(value, index) => 1 + index,
110+
)
111+
},
112+
totalPages() {
113+
return Math.max(1, Math.ceil(this.totalItemsCount / this.limit))
114+
},
115+
pageNumber: {
116+
get() {
117+
return Math.floor(this.offset / this.limit) + 1
118+
},
119+
set(pageNumber) {
120+
this.$emit('update:offset', (pageNumber - 1) * this.limit)
121+
},
122+
},
123+
},
124+
}
125+
</script>
126+
127+
<style lang="scss" scoped>
128+
:deep(.vs__clear) {
129+
display: none;
130+
}
131+
132+
:deep(.v-select) {
133+
min-width: 95px !important;
134+
.vs__dropdown-toggle {
135+
background: none;
136+
}
137+
}
138+
139+
.selected-page {
140+
padding-left: 5px;
141+
142+
display: inline-flex;
143+
align-items: center;
144+
}
145+
146+
.page-number {
147+
padding-inline: 5px;
148+
padding-top: 5px;
149+
padding-bottom: 1px;
150+
}
151+
152+
.pagination-items {
153+
background-color: var(--color-main-background);
154+
border-radius: var(--border-radius-large);
155+
pointer-events: all;
156+
157+
display: flex;
158+
align-items: center;
159+
}
160+
161+
.pagination-block {
162+
width: 100%;
163+
pointer-events: none;
164+
165+
display: flex;
166+
justify-content: center;
167+
align-items: center;
168+
}
169+
</style>

0 commit comments

Comments
 (0)