Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(polls): handle update-drafts API endpoint #14236

Merged
merged 3 commits into from
Jan 30, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -6,23 +6,30 @@
<template>
<!-- Poll card -->
<div v-if="draft" class="poll-card" @click="openDraft">
<span class="poll-card__header">
<IconPoll :size="20" />
<span>{{ name }}</span>
<span class="poll-card__header poll-card__header--draft">
<IconPoll class="poll-card__header-icon" :size="20" />
<span class="poll-card__header-name">{{ name }}</span>
<NcButton v-if="canEditPollDraft"
type="tertiary"
:title="t('spreed', 'Edit poll draft')"
:aria-label="t('spreed', 'Edit poll draft')"
@click.stop="editDraft">
<template #icon>
<IconPencil :size="20" />
</template>
</NcButton>
<NcButton type="tertiary"
:title="t('spreed', 'Delete poll draft')"
:aria-label="t('spreed', 'Delete poll draft')"
@click.stop="deleteDraft">
<template #icon>
<IconDelete :size="20" />
</template>
</NcButton>
</span>
<span class="poll-card__footer">
{{ pollFooterText }}
</span>

<NcButton class="poll-card__delete-draft"
type="tertiary"
:title="t('spreed', 'Delete poll draft')"
:aria-label="t('spreed', 'Delete poll draft')"
@click.stop="deleteDraft">
<template #icon>
<IconDelete :size="20" />
</template>
</NcButton>
</div>
<a v-else-if="!showAsButton"
v-intersection-observer="getPollData"
@@ -31,8 +38,8 @@
role="button"
@click="openPoll">
<span class="poll-card__header">
<IconPoll :size="20" />
<span>{{ name }}</span>
<IconPoll class="poll-card__header-icon" :size="20" />
<span class="poll-card__header-name">{{ name }}</span>
</span>
<span class="poll-card__footer">
{{ pollFooterText }}
@@ -52,13 +59,15 @@
import { vIntersectionObserver as IntersectionObserver } from '@vueuse/components'

import IconDelete from 'vue-material-design-icons/Delete.vue'
import IconPencil from 'vue-material-design-icons/Pencil.vue'
import IconPoll from 'vue-material-design-icons/Poll.vue'

import { t, n } from '@nextcloud/l10n'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'

import { POLL } from '../../../../../constants.ts'
import { hasTalkFeature } from '../../../../../services/CapabilitiesManager.ts'
import { usePollsStore } from '../../../../../stores/polls.ts'

export default {
@@ -67,6 +76,7 @@ export default {
components: {
NcButton,
IconDelete,
IconPencil,
IconPoll,
},

@@ -129,6 +139,10 @@ export default {
: t('spreed', 'Poll')
}
},

canEditPollDraft() {
return this.draft && hasTalkFeature(this.token, 'edit-draft-poll')
}
},

methods: {
@@ -144,7 +158,11 @@ export default {
},

openDraft() {
this.$emit('click', this.id)
this.$emit('click', { id: this.id, action: 'fill' })
},

editDraft() {
this.$emit('click', { id: this.id, action: 'edit' })
},

deleteDraft() {
@@ -186,14 +204,23 @@ export default {
&__header {
display: flex;
align-items: flex-start;
gap: 8px;
gap: calc(2 * var(--default-grid-baseline));
margin-bottom: 16px;
font-weight: bold;
white-space: normal;
word-wrap: anywhere;
margin-inline-end: var(--default-clickable-area);

:deep(.material-design-icon) {
&--draft {
gap: var(--default-grid-baseline);
}

&-name {
margin-inline-end: auto;
align-self: center;
}

&-icon {
height: var(--default-clickable-area);
margin-bottom: auto;
}
}
@@ -202,12 +229,6 @@ export default {
color: var(--color-text-maxcontrast);
white-space: normal;
}

& &__delete-draft {
position: absolute;
top: var(--default-grid-baseline);
inset-inline-end: var(--default-grid-baseline);
}
}

.poll-closed {
10 changes: 6 additions & 4 deletions src/components/PollViewer/PollDraftHandler.vue
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@
@click="openPollEditor" />
</div>
<template v-if="!props.editorOpened" #actions>
<NcButton @click="openPollEditor(null)">
<NcButton @click="openPollEditor({ id: null, action: 'fill' })">
{{ t('spreed', 'Create new poll') }}
</NcButton>
</template>
@@ -73,10 +73,12 @@ const pollDraftsLoaded = computed(() => pollsStore.draftsLoaded(props.token))

/**
* Opens poll editor pre-filled from the draft
* @param id poll draft ID
* @param payload method payload
* @param payload.id poll draft ID
* @param payload.action required action ('fill' from draft or 'edit' draft)
*/
function openPollEditor(id: number | null) {
EventBus.emit('poll-editor-open', { id, fromDrafts: !props.editorOpened, selector: props.container })
function openPollEditor({ id, action } : { id: number | null, action?: string }) {
EventBus.emit('poll-editor-open', { id, fromDrafts: !props.editorOpened, action, selector: props.container })
}
</script>

38 changes: 33 additions & 5 deletions src/components/PollViewer/PollEditor.vue
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
-->

<template>
<NcDialog :name="t('spreed', 'Create new poll')"
<NcDialog :name="dialogName"
:close-on-click-outside="!isFilled"
:container="container"
v-on="$listeners"
@@ -89,7 +89,7 @@
</div>
<template #actions>
<NcActions v-if="supportPollDrafts" force-menu>
<NcActionButton v-if="props.canCreatePollDrafts" :disabled="!isFilled" @click="createPollDraft">
<NcActionButton v-if="props.canCreatePollDrafts && !editingDraftId" :disabled="!isFilled" @click="createPollDraft">
<template #icon>
<IconFileEdit :size="20" />
</template>
@@ -102,7 +102,7 @@
{{ t('spreed', 'Export draft to file') }}
</NcActionLink>
</NcActions>
<NcButton type="primary" :disabled="!isFilled" @click="createPoll">
<NcButton type="primary" :disabled="!isFilled" @click="handleSubmit">
{{ createPollLabel }}
</NcButton>
</template>
@@ -157,6 +157,7 @@ const store = useStore()
const pollsStore = usePollsStore()

const isOpenedFromDraft = ref(false)
const editingDraftId = ref<number | null>(null)
const pollOption = ref<InstanceType<typeof NcTextField>[] | null>(null)
const pollImport = ref<HTMLInputElement | null>(null)

@@ -168,7 +169,14 @@ const pollForm = reactive<createPollParams>({
})

const isFilled = computed(() => Boolean(pollForm.question) && pollForm.options.filter(option => Boolean(option)).length >= 2)
const dialogName = computed(() => {
return editingDraftId.value ? t('spreed', 'Edit poll draft') : t('spreed', 'Create new poll')
})
const createPollLabel = computed(() => {
if (editingDraftId.value) {
return t('spreed', 'Save')
}

return store.getters.getToken() !== props.token
? t('spreed', 'Create poll in {name}', { name: store.getters.conversation(props.token).displayName },
undefined, { escape: false, sanitize: false })
@@ -217,7 +225,22 @@ function addOption() {
/**
* Post a poll into conversation
*/
async function createPoll() {
async function handleSubmit() {
if (editingDraftId.value) {
const pollDraft = await pollsStore.updatePollDraft({
token: props.token,
pollId: editingDraftId.value,
form: pollForm,
})
if (pollDraft) {
openPollDraftHandler()
nextTick(() => {
emit('close')
})
}
return
}

const poll = await pollsStore.createPoll({
token: props.token,
form: pollForm,
@@ -231,12 +254,17 @@ async function createPoll() {
* Pre-fills form from the draft
* @param id poll draft ID
* @param fromDrafts whether editor was opened from drafts handler
* @param action required action ('fill' from draft or 'edit' draft)
*/
function fillPollEditorFromDraft(id: number | null, fromDrafts: boolean) {
function fillPollEditorFromDraft(id: number | null, fromDrafts: boolean, action?: string) {
if (fromDrafts) {
// Show 'Back' button, do not reset until closed
isOpenedFromDraft.value = true
}
if (action === 'edit') {
// Show Edit interface
editingDraftId.value = id
}

if (id === null) {
return
5 changes: 3 additions & 2 deletions src/components/PollViewer/PollManager.vue
Original file line number Diff line number Diff line change
@@ -57,13 +57,14 @@ function openPollDraftHandler({ selector }: Events['poll-drafts-open']) {
* @param payload event payload
* @param payload.id poll draft ID to fill form with (null for empty form)
* @param payload.fromDrafts whether editor was opened from PollDraftHandler dialog
* @param payload.action required action ('fill' from draft or 'edit' draft)
* @param [payload.selector] selector to mount dialog to (body by default)
*/
function openPollEditor({ id, fromDrafts, selector }: Events['poll-editor-open']) {
function openPollEditor({ id, fromDrafts, action, selector }: Events['poll-editor-open']) {
container.value = selector
showPollEditor.value = true
nextTick(() => {
pollEditorRef.value?.fillPollEditorFromDraft(id, fromDrafts)
pollEditorRef.value?.fillPollEditorFromDraft(id, fromDrafts, action)
// Wait for editor to be mounted and filled before unmounting drafts dialog to avoid issues when inserting nodes
showPollDraftHandler.value = false
})
2 changes: 1 addition & 1 deletion src/services/EventBus.ts
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ export type Events = {
'joined-conversation': { token: string },
'message-height-changed': { heightDiff: number },
'poll-drafts-open': { selector?: string },
'poll-editor-open': { id: number | null, fromDrafts: boolean, selector?: string },
'poll-editor-open': { id: number | null, fromDrafts: boolean, action?: string, selector?: string },
'refresh-peer-list': void,
'retry-message': number,
'route-change': { from: Route, to: Route },
22 changes: 22 additions & 0 deletions src/services/pollService.ts
Original file line number Diff line number Diff line change
@@ -16,9 +16,12 @@ import type {
getPollResponse,
votePollParams,
votePollResponse,
updatePollDraftParams,
updatePollDraftResponse,
} from '../types/index.ts'

type createPollPayload = { token: string } & createPollParams
type updatePollDraftPayload = { token: string, pollId: number } & updatePollDraftParams

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

/**
* @param payload The payload
* @param payload.token The conversation token
* @param payload.pollId The id of poll draft
* @param payload.question The question of the poll
* @param payload.options The options participants can vote for
* @param payload.resultMode Result mode of the poll (0 - always visible | 1 - hidden until the poll is closed)
* @param payload.maxVotes Maximum amount of options a user can vote for (0 - unlimited | 1 - single answer)
*/
const updatePollDraft = async ({ token, pollId, question, options, resultMode, maxVotes }: updatePollDraftPayload): updatePollDraftResponse => {
return axios.post(generateOcsUrl('apps/spreed/api/v1/poll/{token}/draft/{pollId}', { token, pollId }), {
question,
options,
resultMode,
maxVotes,
} as updatePollDraftParams)
}

/**
* @param token The conversation token
*/
@@ -100,6 +121,7 @@ const deletePollDraft = async (token: string, pollId: string): deletePollDraftRe
export {
createPoll,
createPollDraft,
updatePollDraft,
getPollDrafts,
getPollData,
submitVote,
15 changes: 15 additions & 0 deletions src/stores/polls.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { t } from '@nextcloud/l10n'
import {
createPoll,
createPollDraft,
updatePollDraft,
getPollDrafts,
getPollData,
submitVote,
@@ -22,6 +23,7 @@ import type {
ChatMessage,
createPollParams,
votePollParams,
updatePollDraftParams,
Poll,
PollDraft,
} from '../types/index.ts'
@@ -154,6 +156,19 @@ export const usePollsStore = defineStore('polls', {
}
},

async updatePollDraft({ token, pollId, form }: { token: string, pollId: number, form: updatePollDraftParams }) {
try {
const response = await updatePollDraft({ token, pollId, ...form })
this.addPollDraft({ token, draft: response.data.ocs.data })

showSuccess(t('spreed', 'Poll draft has been saved'))
return response.data.ocs.data
} catch (error) {
showError(t('spreed', 'An error occurred while saving the draft'))
console.error(error)
}
},

async submitVote({ token, pollId, optionIds }: submitVotePayload) {
try {
const response = await submitVote(token, pollId, optionIds)
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -226,6 +226,8 @@ export type getPollResponse = ApiResponse<operations['poll-show-poll']['response
export type getPollDraftsResponse = ApiResponse<operations['poll-get-all-draft-polls']['responses'][200]['content']['application/json']>
export type createPollParams = operations['poll-create-poll']['requestBody']['content']['application/json']
export type createPollResponse = ApiResponse<operations['poll-create-poll']['responses'][201]['content']['application/json']>
export type updatePollDraftParams = operations['poll-update-draft-poll']['requestBody']['content']['application/json']
export type updatePollDraftResponse = ApiResponse<operations['poll-update-draft-poll']['responses'][200]['content']['application/json']>
export type createPollDraftResponse = ApiResponse<operations['poll-create-poll']['responses'][200]['content']['application/json']>
export type votePollParams = Required<operations['poll-vote-poll']>['requestBody']['content']['application/json']
export type votePollResponse = ApiResponse<operations['poll-vote-poll']['responses'][200]['content']['application/json']>
Loading