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
Show file tree
Hide file tree
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
Expand Up @@ -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"
Expand All @@ -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 }}
Expand All @@ -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 {
Expand All @@ -67,6 +76,7 @@ export default {
components: {
NcButton,
IconDelete,
IconPencil,
IconPoll,
},

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

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

methods: {
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions src/components/PollViewer/PollDraftHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>

Expand Down
38 changes: 33 additions & 5 deletions src/components/PollViewer/PollEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>
Expand All @@ -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">
Antreesy marked this conversation as resolved.
Show resolved Hide resolved
{{ createPollLabel }}
</NcButton>
</template>
Expand Down Expand Up @@ -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)

Expand All @@ -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 })
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/components/PollViewer/PollManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
2 changes: 1 addition & 1 deletion src/services/EventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
22 changes: 22 additions & 0 deletions src/services/pollService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -100,6 +121,7 @@ const deletePollDraft = async (token: string, pollId: string): deletePollDraftRe
export {
createPoll,
createPollDraft,
updatePollDraft,
getPollDrafts,
getPollData,
submitVote,
Expand Down
15 changes: 15 additions & 0 deletions src/stores/polls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { t } from '@nextcloud/l10n'
import {
createPoll,
createPollDraft,
updatePollDraft,
getPollDrafts,
getPollData,
submitVote,
Expand All @@ -22,6 +23,7 @@ import type {
ChatMessage,
createPollParams,
votePollParams,
updatePollDraftParams,
Poll,
PollDraft,
} from '../types/index.ts'
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']>
Expand Down
Loading