Skip to content

Commit e4eea71

Browse files
committed
delete orphaned votes from poll (Frontend)
Signed-off-by: dartcafe <github@dartcafe.de>
1 parent cf4d0d6 commit e4eea71

6 files changed

Lines changed: 124 additions & 50 deletions

File tree

src/Api/modules/api.types.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { Comment, Option, Poll, Share, Vote } from '../../Types'
7+
8+
export type FullPollResponse = {
9+
poll: Poll
10+
options: Option[]
11+
votes: Vote[]
12+
orphaned: number
13+
comments: Comment[]
14+
shares: Share[]
15+
subscribed: boolean
16+
}
17+
18+
export type AddOptionResponse = {
19+
option: Option
20+
repetitions: Option[]
21+
options: Option[]
22+
votes: Vote[]
23+
}
24+
25+
export type setVoteResponse = {
26+
vote: Vote
27+
poll: Poll
28+
options: Option[]
29+
votes: Vote[]
30+
}
31+
32+
export type RemoveVotesResponse = {
33+
poll: Poll
34+
options: Option[]
35+
votes: Vote[]
36+
}

src/Api/modules/polls.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
import { Poll, PollConfiguration, PollType } from '../../stores/poll.ts'
66
import { AxiosResponse } from '@nextcloud/axios'
77
import { httpInstance, createCancelTokenHandler } from './HttpApi.js'
8-
import { Option } from '../../stores/options.ts'
9-
import { Vote } from '../../stores/votes.ts'
10-
import { Share } from '../../stores/shares.ts'
11-
import { ApiEmailAdressList, Comment } from '../../Types/index.ts'
8+
import { ApiEmailAdressList, Vote } from '../../Types/index.ts'
129
import { PollGroup } from '../../stores/pollGroups.types.ts'
10+
import { FullPollResponse } from './api.types.ts'
1311

1412
export type Confirmations = {
1513
sentMails: { emailAddress: string; displayName: string }[]
@@ -52,16 +50,7 @@ const polls = {
5250
})
5351
},
5452

55-
getFullPoll(pollId: number): Promise<
56-
AxiosResponse<{
57-
poll: Poll
58-
options: Option[]
59-
votes: Vote[]
60-
comments: Comment[]
61-
shares: Share[]
62-
subscribed: boolean
63-
}>
64-
> {
53+
getFullPoll(pollId: number): Promise<AxiosResponse<FullPollResponse>> {
6554
return httpInstance.request({
6655
method: 'GET',
6756
url: `poll/${pollId}`,
@@ -150,6 +139,19 @@ const polls = {
150139
})
151140
},
152141

142+
removeOrphanedVotes(
143+
pollId: number,
144+
): Promise<AxiosResponse<{ deleted: Vote[] }>> {
145+
return httpInstance.request({
146+
method: 'DELETE',
147+
url: `poll/${pollId}/votes/orphaned/all`,
148+
cancelToken:
149+
cancelTokenHandlerObject[
150+
this.removeOrphanedVotes.name
151+
].handleRequestCancellation().token,
152+
})
153+
},
154+
153155
closePoll(pollId: number): Promise<AxiosResponse<{ poll: Poll }>> {
154156
return httpInstance.request({
155157
method: 'PUT',

src/Api/modules/public.ts

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,17 @@ import { Session } from '../../stores/session.js'
88
import { Answer, Vote } from '../../stores/votes.js'
99
import { httpInstance, createCancelTokenHandler } from './HttpApi.js'
1010
import { Comment } from '../../stores/comments.js'
11-
import { Poll } from '../../stores/poll.js'
1211
import { Share } from '../../stores/shares.js'
1312
import { SentResults } from './shares.js'
13+
import {
14+
AddOptionResponse,
15+
FullPollResponse,
16+
RemoveVotesResponse,
17+
setVoteResponse,
18+
} from './api.types.js'
1419

1520
const publicPoll = {
16-
getPoll(shareToken: string): Promise<
17-
AxiosResponse<{
18-
poll: Poll
19-
options: Option[]
20-
votes: Vote[]
21-
comments: Comment[]
22-
shares: Share[]
23-
subscribed: boolean
24-
}>
25-
> {
21+
getPoll(shareToken: string): Promise<AxiosResponse<FullPollResponse>> {
2622
return httpInstance.request({
2723
method: 'GET',
2824
url: `/s/${shareToken}/poll`,
@@ -63,14 +59,7 @@ const publicPoll = {
6359
option: SimpleOption,
6460
sequence: Sequence | null,
6561
voteYes: boolean = false,
66-
): Promise<
67-
AxiosResponse<{
68-
option: Option
69-
repetitions: Option[]
70-
options: Option[]
71-
votes: Vote[]
72-
}>
73-
> {
62+
): Promise<AxiosResponse<AddOptionResponse>> {
7463
return httpInstance.request({
7564
method: 'POST',
7665
url: `/s/${shareToken}/option`,
@@ -129,9 +118,7 @@ const publicPoll = {
129118
shareToken: string,
130119
optionId: number,
131120
setTo: Answer,
132-
): Promise<
133-
AxiosResponse<{ vote: Vote; poll: Poll; options: Option[]; votes: Vote[] }>
134-
> {
121+
): Promise<AxiosResponse<setVoteResponse>> {
135122
return httpInstance.request({
136123
method: 'PUT',
137124
url: `s/${shareToken}/vote`,
@@ -146,9 +133,7 @@ const publicPoll = {
146133
})
147134
},
148135

149-
resetVotes(
150-
shareToken: string,
151-
): Promise<AxiosResponse<{ poll: Poll; options: Option[]; votes: Vote[] }>> {
136+
resetVotes(shareToken: string): Promise<AxiosResponse<RemoveVotesResponse>> {
152137
return httpInstance.request({
153138
method: 'DELETE',
154139
url: `s/${shareToken}/user`,
@@ -161,7 +146,7 @@ const publicPoll = {
161146

162147
removeOrphanedVotes(
163148
shareToken: string,
164-
): Promise<AxiosResponse<{ poll: Poll; options: Option[]; votes: Vote[] }>> {
149+
): Promise<AxiosResponse<RemoveVotesResponse>> {
165150
return httpInstance.request({
166151
method: 'DELETE',
167152
url: `s/${shareToken}/votes/orphaned`,

src/Api/modules/votes.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import { AxiosResponse } from '@nextcloud/axios'
66
import { Answer, Vote } from '../../stores/votes.js'
77
import { httpInstance, createCancelTokenHandler } from './HttpApi.js'
8-
import { Option } from '../../stores/options.js'
9-
import { Poll } from '../../stores/poll.js'
8+
import { RemoveVotesResponse, setVoteResponse } from './api.types.js'
109

1110
const votes = {
1211
getVotes(pollId: number): Promise<AxiosResponse<{ votes: Vote[] }>> {
@@ -24,9 +23,7 @@ const votes = {
2423
setVote(
2524
optionId: number,
2625
setTo: Answer,
27-
): Promise<
28-
AxiosResponse<{ vote: Vote; poll: Poll; options: Option[]; votes: Vote[] }>
29-
> {
26+
): Promise<AxiosResponse<setVoteResponse>> {
3027
return httpInstance.request({
3128
method: 'PUT',
3229
url: 'vote',
@@ -44,7 +41,7 @@ const votes = {
4441
resetVotes(
4542
pollId: number,
4643
userId: string | null = null,
47-
): Promise<AxiosResponse<{ poll: Poll; options: Option[]; votes: Vote[] }>> {
44+
): Promise<AxiosResponse<RemoveVotesResponse>> {
4845
return httpInstance.request({
4946
method: 'DELETE',
5047
url: userId ? `poll/${pollId}/user/${userId}` : `poll/${pollId}/user`,
@@ -57,7 +54,7 @@ const votes = {
5754

5855
removeOrphanedVotes(
5956
pollId: number,
60-
): Promise<AxiosResponse<{ poll: Poll; options: Option[]; votes: Vote[] }>> {
57+
): Promise<AxiosResponse<RemoveVotesResponse>> {
6158
return httpInstance.request({
6259
method: 'DELETE',
6360
url: `poll/${pollId}/votes/orphaned`,

src/components/Configuration/ConfigDangerArea.vue

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
-->
55

66
<script setup lang="ts">
7-
import { ref } from 'vue'
7+
import { computed, ref } from 'vue'
88
import { router } from '../../router.ts'
9-
import { t } from '@nextcloud/l10n'
10-
import { showError } from '@nextcloud/dialogs'
9+
import { n, t } from '@nextcloud/l10n'
10+
import { showError, showSuccess } from '@nextcloud/dialogs'
1111
import NcButton from '@nextcloud/vue/components/NcButton'
1212
1313
import { usePollStore } from '../../stores/poll.ts'
@@ -17,6 +17,7 @@ import RestorePollIcon from 'vue-material-design-icons/Recycle.vue'
1717
import ArchivePollIcon from 'vue-material-design-icons/Archive.vue'
1818
import DeletePollIcon from 'vue-material-design-icons/Delete.vue'
1919
import TransferPollIcon from 'vue-material-design-icons/AccountSwitchOutline.vue'
20+
import DeleteOrphanedIcon from 'vue-material-design-icons/CloseBoxMultipleOutline.vue'
2021
import TransferPollDialog from '../Modals/TransferPollDialog.vue'
2122
2223
const pollStore = usePollStore()
@@ -38,6 +39,26 @@ function toggleArchive() {
3839
}
3940
}
4041
42+
const showDeleteOrphaned = computed(
43+
() =>
44+
pollStore.status.orphanedVotes > 0
45+
&& pollStore.permissions.changeForeignVotes,
46+
)
47+
async function deleteOrphaned() {
48+
try {
49+
const deleted = await pollStore.removeOrphanedVotes()
50+
showSuccess(
51+
n(
52+
'polls',
53+
'Removed %n orphaned vote',
54+
'Removed %n orphaned votes',
55+
deleted,
56+
),
57+
)
58+
} catch {
59+
showError(t('polls', 'Error deleting orphaned votes.'))
60+
}
61+
}
4162
function routeAway() {
4263
router.push({
4364
name: 'list',
@@ -80,6 +101,21 @@ function routeAway() {
80101
{{ t('polls', 'Transfer poll') }}
81102
</template>
82103
</NcButton>
104+
<NcButton v-if="showDeleteOrphaned" @click="deleteOrphaned">
105+
<template #icon>
106+
<DeleteOrphanedIcon />
107+
</template>
108+
<template #default>
109+
{{
110+
n(
111+
'polls',
112+
'Remove orphaned vote',
113+
'Remove %n orphaned votes',
114+
pollStore.status.orphanedVotes,
115+
)
116+
}}
117+
</template>
118+
</NcButton>
83119
</div>
84120
<DeletePollDialog
85121
v-model="showDeleteDialog"

src/stores/poll.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type PollStatus = {
9393
deletionDate: number
9494
archivedDate: number
9595
countParticipants: number
96+
orphanedVotes: number
9697
}
9798

9899
export type PollPermissions = {
@@ -193,6 +194,7 @@ export const usePollStore = defineStore('poll', {
193194
deletionDate: 0,
194195
archivedDate: 0,
195196
countParticipants: 0,
197+
orphanedVotes: 0,
196198
},
197199
currentUserStatus: {
198200
groupInvitations: [],
@@ -405,6 +407,7 @@ export const usePollStore = defineStore('poll', {
405407
}
406408

407409
this.$patch(response.data.poll)
410+
this.status.orphanedVotes = response.data.orphaned
408411
votesStore.votes = response.data.votes
409412
optionsStore.options = response.data.options
410413
sharesStore.shares = response.data.shares
@@ -539,6 +542,21 @@ export const usePollStore = defineStore('poll', {
539542
}
540543
},
541544

545+
async removeOrphanedVotes(): Promise<number> {
546+
try {
547+
const response = await PollsAPI.removeOrphanedVotes(this.id)
548+
this.status.orphanedVotes =
549+
this.status.orphanedVotes - response.data.deleted.length
550+
return response.data.deleted.length
551+
} catch (error) {
552+
if ((error as AxiosError)?.code === 'ERR_CANCELED') {
553+
return 0
554+
}
555+
Logger.error('Error deleting orphaned votes', { error })
556+
throw error
557+
}
558+
},
559+
542560
async toggleArchive(payload: { pollId: number }): Promise<void> {
543561
const pollsStore = usePollsStore()
544562

0 commit comments

Comments
 (0)