Skip to content

Commit c8d1016

Browse files
authored
Merge pull request #4106 from nextcloud/enh/edit-poll-groups
2 parents efe05dc + 72639ca commit c8d1016

7 files changed

Lines changed: 362 additions & 13 deletions

File tree

lib/Controller/PollController.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,21 @@ public function addPollToPollGroup(int $pollId, int $pollGroupId): JSONResponse
313313
]);
314314
}
315315

316+
/**
317+
* Update Pollgroup
318+
*/
319+
#[NoAdminRequired]
320+
#[FrontpageRoute(verb: 'PUT', url: '/pollgroup/{pollGroupId}/update')]
321+
public function updatePollGroup(
322+
int $pollGroupId,
323+
string $title,
324+
string $titleExt,
325+
string $description,
326+
): JSONResponse {
327+
return $this->response(fn () => [
328+
'pollGroup' => $this->pollService->updatePollGroup($pollGroupId, $title, $titleExt, $description),
329+
]);
330+
}
316331
/**
317332
* Remove poll from pollgroup
318333
* @param int $pollId Poll id

lib/Service/PollService.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ public function listPollGroups(): array {
7272
return $this->pollGroupMapper->list();
7373
}
7474

75+
public function updatePollGroup(
76+
int $pollGroupId,
77+
string $title,
78+
string $titleExt,
79+
string $description,
80+
): PollGroup {
81+
try {
82+
$pollGroup = $this->pollGroupMapper->find($pollGroupId);
83+
if ($pollGroup->getOwner() !== $this->userSession->getCurrentUserId()) {
84+
throw new ForbiddenException('You do not have permission to edit this poll group');
85+
}
86+
$pollGroup->setTitle($title);
87+
$pollGroup->setTitleExt($titleExt);
88+
$pollGroup->setDescription($description);
89+
90+
$pollGroup = $this->pollGroupMapper->update($pollGroup);
91+
return $pollGroup;
92+
} catch (DoesNotExistException $e) {
93+
throw new NotFoundException('Poll group not found');
94+
}
95+
}
7596
public function addPollToPollGroup(
7697
int $pollId,
7798
?int $pollGroupId = null,

src/Api/modules/polls.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ const polls = {
7676
})
7777
},
7878

79+
updatePollGroup(
80+
pollGroupId: number,
81+
title: string,
82+
titleExt: string,
83+
description: string,
84+
): Promise<AxiosResponse<{ pollGroup: PollGroup }>> {
85+
return httpInstance.request({
86+
method: 'PUT',
87+
url: `pollgroup/${pollGroupId}/update`,
88+
data: {
89+
title,
90+
titleExt,
91+
description,
92+
},
93+
cancelToken:
94+
cancelTokenHandlerObject[
95+
this.updatePollGroup.name
96+
].handleRequestCancellation().token,
97+
})
98+
},
99+
79100
getPolls(): Promise<
80101
AxiosResponse<{
81102
polls: Poll[]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2021 Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { ref } from 'vue'
8+
9+
import { useSessionStore } from '../../../stores/session'
10+
11+
import { t } from '@nextcloud/l10n'
12+
13+
import ButtonModal from '../../Base/modules/ButtonModal.vue'
14+
import { ButtonMode } from '../../../Types'
15+
import PollGroupEditDlg from '../../PollGroup/PollGroupEditDlg.vue'
16+
17+
import EditIcon from 'vue-material-design-icons/Pencil.vue'
18+
19+
interface Props {
20+
caption?: string
21+
modalSize?: string
22+
buttonMode?: ButtonMode
23+
}
24+
25+
const {
26+
caption = t('polls', 'Edit poll group'),
27+
modalSize = 'normal',
28+
buttonMode = ButtonMode.Native,
29+
} = defineProps<Props>()
30+
31+
const sessionStore = useSessionStore()
32+
33+
const showModal = ref(false)
34+
35+
function updatedGroup() {
36+
// close modal and show the confirmation dialog
37+
showModal.value = false
38+
}
39+
</script>
40+
41+
<template>
42+
<ButtonModal
43+
v-if="sessionStore.appPermissions.pollCreation"
44+
v-model:show-modal="showModal"
45+
:aria-label="caption"
46+
:button-caption="caption"
47+
:modal-size="modalSize"
48+
:button-mode="buttonMode"
49+
:button-variant="'secondary'">
50+
<template #icon>
51+
<EditIcon size="20" decorative />
52+
</template>
53+
<template #modal-content>
54+
<PollGroupEditDlg @updated="updatedGroup" @close="showModal = false" />
55+
</template>
56+
</ButtonModal>
57+
</template>
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2018 Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { computed, ref, watch } from 'vue'
8+
import { t } from '@nextcloud/l10n'
9+
10+
import NcButton from '@nextcloud/vue/components/NcButton'
11+
12+
import SpeakerIcon from 'vue-material-design-icons/Bullhorn.vue'
13+
import SpeakerBigIcon from 'vue-material-design-icons/BullhornVariant.vue'
14+
import DescriptionIcon from 'vue-material-design-icons/TextBox.vue'
15+
16+
import { ConfigBox, InputDiv } from '../Base/index.ts'
17+
18+
import { usePollsStore } from '../../stores/polls.ts'
19+
import { showError } from '@nextcloud/dialogs'
20+
import { useRoute } from 'vue-router'
21+
import { router } from '../../router.ts'
22+
23+
const pollsStore = usePollsStore()
24+
const route = useRoute()
25+
const helperTexts = {
26+
title: t('polls', 'Choose a brief title for the navigation bar and the slug'),
27+
titleExt: t('polls', 'Choose a more meaningful title for the overview page'),
28+
description: t('polls', 'Choose a description for the overview page'),
29+
titleChangedNote: t('polls', 'Note: Changing the title, also changes the URL'),
30+
}
31+
const emit = defineEmits<{
32+
(e: 'close'): void
33+
(e: 'updated'): void
34+
}>()
35+
36+
const newGroupAttributes = ref({
37+
slug: route.params.slug as string,
38+
title: pollsStore.currentGroup?.title || '',
39+
titleExt: pollsStore.currentGroup?.titleExt || '',
40+
description: pollsStore.currentGroup?.description || '',
41+
})
42+
43+
watch(route, (route) => {
44+
resetInputs(route.params.slug as string)
45+
})
46+
47+
const pollGroupTitle = computed({
48+
get() {
49+
return pollsStore.currentGroup?.title || ''
50+
},
51+
set(newTitle: string) {
52+
newGroupAttributes.value.title = newTitle
53+
},
54+
})
55+
const pollGroupTitleExt = computed({
56+
get() {
57+
return pollsStore.currentGroup?.titleExt || ''
58+
},
59+
set(newTitleExt: string) {
60+
newGroupAttributes.value.titleExt = newTitleExt
61+
},
62+
})
63+
64+
const pollGroupTitleDescription = computed({
65+
get() {
66+
return pollsStore.currentGroup?.description || ''
67+
},
68+
set(newDescription: string) {
69+
newGroupAttributes.value.description = newDescription
70+
},
71+
})
72+
73+
const updating = ref(false)
74+
75+
const titleUpdated = computed(
76+
() => pollGroupTitle.value !== pollsStore.currentGroup?.title,
77+
)
78+
const titleIsEmpty = computed(() => pollGroupTitle.value === '')
79+
const disableEditButton = computed(() => titleIsEmpty.value || updating.value)
80+
81+
function resetInputs(slug: string) {
82+
newGroupAttributes.value = {
83+
slug,
84+
title: pollsStore.currentGroup?.title || '',
85+
titleExt: pollsStore.currentGroup?.titleExt || '',
86+
description: pollsStore.currentGroup?.description || '',
87+
}
88+
}
89+
90+
async function updatePollGroup() {
91+
try {
92+
// block the modal to prevent double submission
93+
updating.value = true
94+
// add the poll
95+
const pollGroup = await pollsStore.updatePollGroup(newGroupAttributes.value)
96+
97+
if (pollGroup) {
98+
resetInputs(route.params.slug as string)
99+
emit('updated')
100+
router.push({
101+
name: 'group',
102+
params: { slug: pollGroup.slug },
103+
})
104+
}
105+
} catch {
106+
showError(t('polls', 'Error updating PollGroup'))
107+
} finally {
108+
// unblock the modal
109+
updating.value = false
110+
}
111+
}
112+
</script>
113+
114+
<template>
115+
<div class="edit-poll-group">
116+
<ConfigBox :name="t('polls', 'Title')">
117+
<template #icon>
118+
<SpeakerIcon />
119+
</template>
120+
<InputDiv
121+
v-model="pollGroupTitle"
122+
focus
123+
type="text"
124+
:placeholder="t('polls', 'Enter Title')"
125+
:helper-text="helperTexts.title"
126+
@submit="updatePollGroup" />
127+
128+
<div class="change-title-hint">
129+
<p v-if="titleUpdated">
130+
{{ helperTexts.titleChangedNote }}
131+
</p>
132+
</div>
133+
</ConfigBox>
134+
135+
<ConfigBox :name="t('polls', 'Extended title')">
136+
<template #icon>
137+
<SpeakerBigIcon />
138+
</template>
139+
<InputDiv
140+
v-model="pollGroupTitleExt"
141+
type="text"
142+
:placeholder="t('polls', 'Enter extended Title')"
143+
:helper-text="helperTexts.titleExt"
144+
@submit="updatePollGroup" />
145+
</ConfigBox>
146+
147+
<ConfigBox :name="t('polls', 'Description')">
148+
<template #icon>
149+
<DescriptionIcon />
150+
</template>
151+
<textarea
152+
v-model="pollGroupTitleDescription"
153+
class="input-textarea"
154+
:placeholder="t('polls', 'Enter a description')" />
155+
<p class="helper">
156+
{{ helperTexts.description }}
157+
</p>
158+
</ConfigBox>
159+
160+
<div class="create-buttons">
161+
<NcButton @click="emit('close')">
162+
<template #default>
163+
{{ t('polls', 'Close') }}
164+
</template>
165+
</NcButton>
166+
<NcButton
167+
:disabled="disableEditButton"
168+
:variant="'primary'"
169+
@click="updatePollGroup">
170+
<template #default>
171+
{{ t('polls', 'Update') }}
172+
</template>
173+
</NcButton>
174+
</div>
175+
</div>
176+
</template>
177+
178+
<style lang="scss">
179+
.edit-poll-group {
180+
background-color: var(--color-main-background);
181+
padding: 8px 20px;
182+
183+
.create-buttons {
184+
display: flex;
185+
justify-content: flex-end;
186+
gap: 8px;
187+
}
188+
189+
.input-textarea {
190+
width: 99%;
191+
resize: vertical;
192+
}
193+
194+
.helper {
195+
min-height: 1.5rem;
196+
font-size: 0.8em;
197+
opacity: 0.8;
198+
}
199+
}
200+
</style>

0 commit comments

Comments
 (0)