Skip to content

Commit acbde75

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

8 files changed

Lines changed: 303 additions & 178 deletions

File tree

docs/API_v3.md

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

658661
```
@@ -741,7 +744,8 @@ Get all Submissions to a Form
741744
"options": [],
742745
"extraSettings": {}
743746
}
744-
]
747+
],
748+
"totalSubmissionsCount": 40
745749
}
746750
```
747751

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: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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 NcButton from '@nextcloud/vue/components/NcButton'
70+
import NcSelect from '@nextcloud/vue/components/NcSelect'
71+
import PageFirstIcon from 'vue-material-design-icons/PageFirst.vue'
72+
import PageLastIcon from 'vue-material-design-icons/PageLast.vue'
73+
74+
export default {
75+
name: 'PaginationToolbar',
76+
emits: ['update:offset'],
77+
components: {
78+
IconChevronLeft,
79+
IconChevronRight,
80+
NcButton,
81+
NcSelect,
82+
PageFirstIcon,
83+
PageLastIcon,
84+
},
85+
86+
props: {
87+
totalItemsCount: {
88+
type: Number,
89+
required: true,
90+
},
91+
limit: {
92+
type: Number,
93+
default: 20,
94+
},
95+
offset: {
96+
type: Number,
97+
default: 0,
98+
},
99+
},
100+
101+
computed: {
102+
allPageNumbersArray() {
103+
return Array.from(
104+
{ length: this.totalPages },
105+
(value, index) => 1 + index,
106+
)
107+
},
108+
totalPages() {
109+
return Math.max(1, Math.ceil(this.totalItemsCount / this.limit))
110+
},
111+
pageNumber: {
112+
get() {
113+
return Math.floor(this.offset / this.limit) + 1
114+
},
115+
set(pageNumber) {
116+
this.$emit('update:offset', (pageNumber - 1) * this.limit)
117+
},
118+
},
119+
},
120+
}
121+
</script>
122+
123+
<style lang="scss" scoped>
124+
:deep(.vs__clear) {
125+
display: none;
126+
}
127+
128+
:deep(.v-select) {
129+
min-width: 95px !important;
130+
.vs__dropdown-toggle {
131+
background: none;
132+
}
133+
}
134+
135+
.selected-page {
136+
padding-left: 5px;
137+
138+
display: inline-flex;
139+
align-items: center;
140+
}
141+
142+
.page-number {
143+
padding-inline: 5px;
144+
padding-top: 5px;
145+
padding-bottom: 1px;
146+
}
147+
148+
.pagination-items {
149+
background-color: var(--color-main-background);
150+
border-radius: var(--border-radius-large);
151+
pointer-events: all;
152+
153+
display: flex;
154+
align-items: center;
155+
}
156+
157+
.pagination-block {
158+
width: 100%;
159+
pointer-events: none;
160+
161+
display: flex;
162+
justify-content: center;
163+
align-items: center;
164+
}
165+
</style>

src/components/Results/Submission.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export default {
6666
},
6767
highlight: {
6868
type: String,
69+
default: null,
6970
},
7071
},
7172

0 commit comments

Comments
 (0)