Skip to content

Commit d1c7079

Browse files
authored
Merge pull request #14236 from nextcloud/feat/noid/edit-drafts-frontend
2 parents e3b852b + 774682d commit d1c7079

File tree

8 files changed

+128
-37
lines changed

8 files changed

+128
-37
lines changed

src/components/MessagesList/MessagesGroup/Message/MessagePart/Poll.vue

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,30 @@
66
<template>
77
<!-- Poll card -->
88
<div v-if="draft" class="poll-card" @click="openDraft">
9-
<span class="poll-card__header">
10-
<IconPoll :size="20" />
11-
<span>{{ name }}</span>
9+
<span class="poll-card__header poll-card__header--draft">
10+
<IconPoll class="poll-card__header-icon" :size="20" />
11+
<span class="poll-card__header-name">{{ name }}</span>
12+
<NcButton v-if="canEditPollDraft"
13+
type="tertiary"
14+
:title="t('spreed', 'Edit poll draft')"
15+
:aria-label="t('spreed', 'Edit poll draft')"
16+
@click.stop="editDraft">
17+
<template #icon>
18+
<IconPencil :size="20" />
19+
</template>
20+
</NcButton>
21+
<NcButton type="tertiary"
22+
:title="t('spreed', 'Delete poll draft')"
23+
:aria-label="t('spreed', 'Delete poll draft')"
24+
@click.stop="deleteDraft">
25+
<template #icon>
26+
<IconDelete :size="20" />
27+
</template>
28+
</NcButton>
1229
</span>
1330
<span class="poll-card__footer">
1431
{{ pollFooterText }}
1532
</span>
16-
17-
<NcButton class="poll-card__delete-draft"
18-
type="tertiary"
19-
:title="t('spreed', 'Delete poll draft')"
20-
:aria-label="t('spreed', 'Delete poll draft')"
21-
@click.stop="deleteDraft">
22-
<template #icon>
23-
<IconDelete :size="20" />
24-
</template>
25-
</NcButton>
2633
</div>
2734
<a v-else-if="!showAsButton"
2835
v-intersection-observer="getPollData"
@@ -31,8 +38,8 @@
3138
role="button"
3239
@click="openPoll">
3340
<span class="poll-card__header">
34-
<IconPoll :size="20" />
35-
<span>{{ name }}</span>
41+
<IconPoll class="poll-card__header-icon" :size="20" />
42+
<span class="poll-card__header-name">{{ name }}</span>
3643
</span>
3744
<span class="poll-card__footer">
3845
{{ pollFooterText }}
@@ -52,13 +59,15 @@
5259
import { vIntersectionObserver as IntersectionObserver } from '@vueuse/components'
5360
5461
import IconDelete from 'vue-material-design-icons/Delete.vue'
62+
import IconPencil from 'vue-material-design-icons/Pencil.vue'
5563
import IconPoll from 'vue-material-design-icons/Poll.vue'
5664
5765
import { t, n } from '@nextcloud/l10n'
5866
5967
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
6068
6169
import { POLL } from '../../../../../constants.ts'
70+
import { hasTalkFeature } from '../../../../../services/CapabilitiesManager.ts'
6271
import { usePollsStore } from '../../../../../stores/polls.ts'
6372
6473
export default {
@@ -67,6 +76,7 @@ export default {
6776
components: {
6877
NcButton,
6978
IconDelete,
79+
IconPencil,
7080
IconPoll,
7181
},
7282
@@ -129,6 +139,10 @@ export default {
129139
: t('spreed', 'Poll')
130140
}
131141
},
142+
143+
canEditPollDraft() {
144+
return this.draft && hasTalkFeature(this.token, 'edit-draft-poll')
145+
}
132146
},
133147
134148
methods: {
@@ -144,7 +158,11 @@ export default {
144158
},
145159
146160
openDraft() {
147-
this.$emit('click', this.id)
161+
this.$emit('click', { id: this.id, action: 'fill' })
162+
},
163+
164+
editDraft() {
165+
this.$emit('click', { id: this.id, action: 'edit' })
148166
},
149167
150168
deleteDraft() {
@@ -186,14 +204,23 @@ export default {
186204
&__header {
187205
display: flex;
188206
align-items: flex-start;
189-
gap: 8px;
207+
gap: calc(2 * var(--default-grid-baseline));
190208
margin-bottom: 16px;
191209
font-weight: bold;
192210
white-space: normal;
193211
word-wrap: anywhere;
194-
margin-inline-end: var(--default-clickable-area);
195212
196-
:deep(.material-design-icon) {
213+
&--draft {
214+
gap: var(--default-grid-baseline);
215+
}
216+
217+
&-name {
218+
margin-inline-end: auto;
219+
align-self: center;
220+
}
221+
222+
&-icon {
223+
height: var(--default-clickable-area);
197224
margin-bottom: auto;
198225
}
199226
}
@@ -202,12 +229,6 @@ export default {
202229
color: var(--color-text-maxcontrast);
203230
white-space: normal;
204231
}
205-
206-
& &__delete-draft {
207-
position: absolute;
208-
top: var(--default-grid-baseline);
209-
inset-inline-end: var(--default-grid-baseline);
210-
}
211232
}
212233
213234
.poll-closed {

src/components/PollViewer/PollDraftHandler.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
@click="openPollEditor" />
3131
</div>
3232
<template v-if="!props.editorOpened" #actions>
33-
<NcButton @click="openPollEditor(null)">
33+
<NcButton @click="openPollEditor({ id: null, action: 'fill' })">
3434
{{ t('spreed', 'Create new poll') }}
3535
</NcButton>
3636
</template>
@@ -73,10 +73,12 @@ const pollDraftsLoaded = computed(() => pollsStore.draftsLoaded(props.token))
7373
7474
/**
7575
* Opens poll editor pre-filled from the draft
76-
* @param id poll draft ID
76+
* @param payload method payload
77+
* @param payload.id poll draft ID
78+
* @param payload.action required action ('fill' from draft or 'edit' draft)
7779
*/
78-
function openPollEditor(id: number | null) {
79-
EventBus.emit('poll-editor-open', { id, fromDrafts: !props.editorOpened, selector: props.container })
80+
function openPollEditor({ id, action } : { id: number | null, action?: string }) {
81+
EventBus.emit('poll-editor-open', { id, fromDrafts: !props.editorOpened, action, selector: props.container })
8082
}
8183
</script>
8284

src/components/PollViewer/PollEditor.vue

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
-->
55

66
<template>
7-
<NcDialog :name="t('spreed', 'Create new poll')"
7+
<NcDialog :name="dialogName"
88
:close-on-click-outside="!isFilled"
99
:container="container"
1010
v-on="$listeners"
@@ -89,7 +89,7 @@
8989
</div>
9090
<template #actions>
9191
<NcActions v-if="supportPollDrafts" force-menu>
92-
<NcActionButton v-if="props.canCreatePollDrafts" :disabled="!isFilled" @click="createPollDraft">
92+
<NcActionButton v-if="props.canCreatePollDrafts && !editingDraftId" :disabled="!isFilled" @click="createPollDraft">
9393
<template #icon>
9494
<IconFileEdit :size="20" />
9595
</template>
@@ -102,7 +102,7 @@
102102
{{ t('spreed', 'Export draft to file') }}
103103
</NcActionLink>
104104
</NcActions>
105-
<NcButton type="primary" :disabled="!isFilled" @click="createPoll">
105+
<NcButton type="primary" :disabled="!isFilled" @click="handleSubmit">
106106
{{ createPollLabel }}
107107
</NcButton>
108108
</template>
@@ -157,6 +157,7 @@ const store = useStore()
157157
const pollsStore = usePollsStore()
158158
159159
const isOpenedFromDraft = ref(false)
160+
const editingDraftId = ref<number | null>(null)
160161
const pollOption = ref<InstanceType<typeof NcTextField>[] | null>(null)
161162
const pollImport = ref<HTMLInputElement | null>(null)
162163
@@ -168,7 +169,14 @@ const pollForm = reactive<createPollParams>({
168169
})
169170
170171
const isFilled = computed(() => Boolean(pollForm.question) && pollForm.options.filter(option => Boolean(option)).length >= 2)
172+
const dialogName = computed(() => {
173+
return editingDraftId.value ? t('spreed', 'Edit poll draft') : t('spreed', 'Create new poll')
174+
})
171175
const createPollLabel = computed(() => {
176+
if (editingDraftId.value) {
177+
return t('spreed', 'Save')
178+
}
179+
172180
return store.getters.getToken() !== props.token
173181
? t('spreed', 'Create poll in {name}', { name: store.getters.conversation(props.token).displayName },
174182
undefined, { escape: false, sanitize: false })
@@ -217,7 +225,22 @@ function addOption() {
217225
/**
218226
* Post a poll into conversation
219227
*/
220-
async function createPoll() {
228+
async function handleSubmit() {
229+
if (editingDraftId.value) {
230+
const pollDraft = await pollsStore.updatePollDraft({
231+
token: props.token,
232+
pollId: editingDraftId.value,
233+
form: pollForm,
234+
})
235+
if (pollDraft) {
236+
openPollDraftHandler()
237+
nextTick(() => {
238+
emit('close')
239+
})
240+
}
241+
return
242+
}
243+
221244
const poll = await pollsStore.createPoll({
222245
token: props.token,
223246
form: pollForm,
@@ -231,12 +254,17 @@ async function createPoll() {
231254
* Pre-fills form from the draft
232255
* @param id poll draft ID
233256
* @param fromDrafts whether editor was opened from drafts handler
257+
* @param action required action ('fill' from draft or 'edit' draft)
234258
*/
235-
function fillPollEditorFromDraft(id: number | null, fromDrafts: boolean) {
259+
function fillPollEditorFromDraft(id: number | null, fromDrafts: boolean, action?: string) {
236260
if (fromDrafts) {
237261
// Show 'Back' button, do not reset until closed
238262
isOpenedFromDraft.value = true
239263
}
264+
if (action === 'edit') {
265+
// Show Edit interface
266+
editingDraftId.value = id
267+
}
240268
241269
if (id === null) {
242270
return

src/components/PollViewer/PollManager.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,14 @@ function openPollDraftHandler({ selector }: Events['poll-drafts-open']) {
5757
* @param payload event payload
5858
* @param payload.id poll draft ID to fill form with (null for empty form)
5959
* @param payload.fromDrafts whether editor was opened from PollDraftHandler dialog
60+
* @param payload.action required action ('fill' from draft or 'edit' draft)
6061
* @param [payload.selector] selector to mount dialog to (body by default)
6162
*/
62-
function openPollEditor({ id, fromDrafts, selector }: Events['poll-editor-open']) {
63+
function openPollEditor({ id, fromDrafts, action, selector }: Events['poll-editor-open']) {
6364
container.value = selector
6465
showPollEditor.value = true
6566
nextTick(() => {
66-
pollEditorRef.value?.fillPollEditorFromDraft(id, fromDrafts)
67+
pollEditorRef.value?.fillPollEditorFromDraft(id, fromDrafts, action)
6768
// Wait for editor to be mounted and filled before unmounting drafts dialog to avoid issues when inserting nodes
6869
showPollDraftHandler.value = false
6970
})

src/services/EventBus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type Events = {
2525
'joined-conversation': { token: string },
2626
'message-height-changed': { heightDiff: number },
2727
'poll-drafts-open': { selector?: string },
28-
'poll-editor-open': { id: number | null, fromDrafts: boolean, selector?: string },
28+
'poll-editor-open': { id: number | null, fromDrafts: boolean, action?: string, selector?: string },
2929
'refresh-peer-list': void,
3030
'retry-message': number,
3131
'route-change': { from: Route, to: Route },

src/services/pollService.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import type {
1616
getPollResponse,
1717
votePollParams,
1818
votePollResponse,
19+
updatePollDraftParams,
20+
updatePollDraftResponse,
1921
} from '../types/index.ts'
2022

2123
type createPollPayload = { token: string } & createPollParams
24+
type updatePollDraftPayload = { token: string, pollId: number } & updatePollDraftParams
2225

2326
/**
2427
* @param payload The payload
@@ -56,6 +59,24 @@ const createPollDraft = async ({ token, question, options, resultMode, maxVotes
5659
} as createPollParams)
5760
}
5861

62+
/**
63+
* @param payload The payload
64+
* @param payload.token The conversation token
65+
* @param payload.pollId The id of poll draft
66+
* @param payload.question The question of the poll
67+
* @param payload.options The options participants can vote for
68+
* @param payload.resultMode Result mode of the poll (0 - always visible | 1 - hidden until the poll is closed)
69+
* @param payload.maxVotes Maximum amount of options a user can vote for (0 - unlimited | 1 - single answer)
70+
*/
71+
const updatePollDraft = async ({ token, pollId, question, options, resultMode, maxVotes }: updatePollDraftPayload): updatePollDraftResponse => {
72+
return axios.post(generateOcsUrl('apps/spreed/api/v1/poll/{token}/draft/{pollId}', { token, pollId }), {
73+
question,
74+
options,
75+
resultMode,
76+
maxVotes,
77+
} as updatePollDraftParams)
78+
}
79+
5980
/**
6081
* @param token The conversation token
6182
*/
@@ -100,6 +121,7 @@ const deletePollDraft = async (token: string, pollId: string): deletePollDraftRe
100121
export {
101122
createPoll,
102123
createPollDraft,
124+
updatePollDraft,
103125
getPollDrafts,
104126
getPollData,
105127
submitVote,

src/stores/polls.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { t } from '@nextcloud/l10n'
1212
import {
1313
createPoll,
1414
createPollDraft,
15+
updatePollDraft,
1516
getPollDrafts,
1617
getPollData,
1718
submitVote,
@@ -22,6 +23,7 @@ import type {
2223
ChatMessage,
2324
createPollParams,
2425
votePollParams,
26+
updatePollDraftParams,
2527
Poll,
2628
PollDraft,
2729
} from '../types/index.ts'
@@ -154,6 +156,19 @@ export const usePollsStore = defineStore('polls', {
154156
}
155157
},
156158

159+
async updatePollDraft({ token, pollId, form }: { token: string, pollId: number, form: updatePollDraftParams }) {
160+
try {
161+
const response = await updatePollDraft({ token, pollId, ...form })
162+
this.addPollDraft({ token, draft: response.data.ocs.data })
163+
164+
showSuccess(t('spreed', 'Poll draft has been saved'))
165+
return response.data.ocs.data
166+
} catch (error) {
167+
showError(t('spreed', 'An error occurred while saving the draft'))
168+
console.error(error)
169+
}
170+
},
171+
157172
async submitVote({ token, pollId, optionIds }: submitVotePayload) {
158173
try {
159174
const response = await submitVote(token, pollId, optionIds)

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ export type getPollResponse = ApiResponse<operations['poll-show-poll']['response
226226
export type getPollDraftsResponse = ApiResponse<operations['poll-get-all-draft-polls']['responses'][200]['content']['application/json']>
227227
export type createPollParams = operations['poll-create-poll']['requestBody']['content']['application/json']
228228
export type createPollResponse = ApiResponse<operations['poll-create-poll']['responses'][201]['content']['application/json']>
229+
export type updatePollDraftParams = operations['poll-update-draft-poll']['requestBody']['content']['application/json']
230+
export type updatePollDraftResponse = ApiResponse<operations['poll-update-draft-poll']['responses'][200]['content']['application/json']>
229231
export type createPollDraftResponse = ApiResponse<operations['poll-create-poll']['responses'][200]['content']['application/json']>
230232
export type votePollParams = Required<operations['poll-vote-poll']>['requestBody']['content']['application/json']
231233
export type votePollResponse = ApiResponse<operations['poll-vote-poll']['responses'][200]['content']['application/json']>

0 commit comments

Comments
 (0)