Skip to content

Commit

Permalink
Merge pull request #13487 from nextcloud/feat/13439/import-polls
Browse files Browse the repository at this point in the history
  • Loading branch information
Antreesy authored Oct 21, 2024
2 parents da8a7a9 + fe543af commit ed6ad0d
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 8 deletions.
59 changes: 59 additions & 0 deletions src/components/NewMessage/NewMessagePollEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,25 @@
</p>
<div class="poll-editor__wrapper">
<NcTextField :value.sync="pollForm.question" :label="t('spreed', 'Ask a question')" v-on="$listeners" />
<!--native file picker, hidden -->
<input id="poll-upload"
ref="pollImport"
type="file"
class="hidden-visually"
@change="importPoll">
<NcActions v-if="supportPollDrafts" force-menu>
<NcActionButton v-if="isModerator" close-after-click @click="openPollDraftHandler">
<template #icon>
<IconFileEdit :size="20" />
</template>
{{ t('spreed', 'Browse poll drafts') }}
</NcActionButton>
<NcActionButton close-after-click @click="triggerImport">
<template #icon>
<IconFileUpload :size="20" />
</template>
{{ t('spreed', 'Import draft from file') }}
</NcActionButton>
</NcActions>
</div>

Expand Down Expand Up @@ -82,6 +94,12 @@
</template>
{{ t('spreed', 'Save as draft') }}
</NcActionButton>
<NcActionLink :href="exportPollURI" :download="exportPollFileName">
<template #icon>
<IconFileDownload :size="20" />
</template>
{{ t('spreed', 'Export draft to file') }}
</NcActionLink>
</NcActions>
<NcButton type="primary" :disabled="!isFilled" @click="createPoll">
{{ t('spreed', 'Create poll') }}
Expand All @@ -95,12 +113,16 @@ import { computed, nextTick, reactive, ref } from 'vue'
import IconArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import Close from 'vue-material-design-icons/Close.vue'
import IconFileDownload from 'vue-material-design-icons/FileDownload.vue'
import IconFileEdit from 'vue-material-design-icons/FileEdit.vue'
import IconFileUpload from 'vue-material-design-icons/FileUpload.vue'
import Plus from 'vue-material-design-icons/Plus.vue'
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
Expand All @@ -113,6 +135,8 @@ import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { EventBus } from '../../services/EventBus.js'
import { usePollsStore } from '../../stores/polls.ts'
import type { createPollParams } from '../../types/index.ts'
import { convertToJSONDataURI } from '../../utils/fileDownload.ts'
import { validatePollForm } from '../../utils/validatePollForm.ts'
const props = defineProps<{
token: string,
Expand All @@ -131,6 +155,7 @@ const pollsStore = usePollsStore()
const isOpenedFromDraft = ref(false)
const pollOption = ref(null)
const pollImport = ref(null)
const pollForm = reactive<createPollParams>({
question: '',
Expand Down Expand Up @@ -160,6 +185,10 @@ const isMultipleAnswer = computed({
})
const isModerator = computed(() => (store.getters as unknown).isModerator)
const exportPollURI = computed(() => convertToJSONDataURI(pollForm))
const exportPollFileName = `Talk Poll ${new Date().toISOString().slice(0, 10)}`
/**
* Remove a previously added option
* @param index option index
Expand Down Expand Up @@ -206,6 +235,36 @@ function fillPollEditorFromDraft(id: number|null, isAlreadyOpened: boolean) {
}
}
/**
* Call native input[type='file'] to import a file
*/
function triggerImport() {
pollImport.value.click()
}
/**
* Validate imported file and insert data into form fields
* @param event import event
*/
function importPoll(event: Event) {
if (!(event.target as HTMLInputElement).files?.[0]) {
return
}
const reader = new FileReader()
reader.onload = (e: ProgressEvent) => {
try {
const parsedObject = validatePollForm(JSON.parse((e.target as FileReader).result as string))
fillPollForm(parsedObject)
} catch (error) {
showError(t('spreed', 'Error while importing poll'))
console.error('Error while importing poll:', error)
}
}
reader.readAsText((event.target as HTMLInputElement).files[0])
}
/**
* Insert data into form fields
* @param payload data to fill with
Expand Down
44 changes: 36 additions & 8 deletions src/components/PollViewer/PollViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
</template>
{{ t('spreed', 'Save as draft') }}
</NcActionButton>
<NcActionLink v-if="supportPollDrafts" :href="exportPollURI" :download="exportPollFileName">
<template #icon>
<IconFileDownload :size="20" />
</template>
{{ t('spreed', 'Export draft to file') }}
</NcActionLink>
<NcActionButton class="critical" @click="endPoll">
{{ t('spreed', 'End poll') }}
<template #icon>
Expand All @@ -86,13 +92,21 @@
</NcActionButton>
</NcActions>
</div>
<div v-else-if="supportPollDrafts && isModerator" class="poll-modal__actions">
<NcButton type="tertiary" @click="createPollDraft">
<template #icon>
<IconFileEdit :size="20" />
</template>
{{ t('spreed', 'Save as draft') }}
</NcButton>
<div v-else-if="supportPollDrafts && selfIsOwnerOrModerator" class="poll-modal__actions">
<NcActions force-menu>
<NcActionButton v-if="isModerator" @click="createPollDraft">
<template #icon>
<IconFileEdit :size="20" />
</template>
{{ t('spreed', 'Save as draft') }}
</NcActionButton>
<NcActionLink :href="exportPollURI" :download="exportPollFileName">
<template #icon>
<IconFileDownload :size="20" />
</template>
{{ t('spreed', 'Export draft to file') }}
</NcActionLink>
</NcActions>
</div>
</div>
<NcLoadingIcon v-else class="poll-modal__loading" />
Expand All @@ -102,14 +116,15 @@
<script>
import { computed, ref } from 'vue'
import IconFileDownload from 'vue-material-design-icons/FileDownload.vue'
import IconFileEdit from 'vue-material-design-icons/FileEdit.vue'
import FileLock from 'vue-material-design-icons/FileLock.vue'
import PollIcon from 'vue-material-design-icons/Poll.vue'
import { showSuccess } from '@nextcloud/dialogs'
import { t, n } from '@nextcloud/l10n'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
Expand All @@ -125,13 +140,15 @@ import { POLL } from '../../constants.js'
import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { EventBus } from '../../services/EventBus.js'
import { usePollsStore } from '../../stores/polls.ts'
import { convertToJSONDataURI } from '../../utils/fileDownload.ts'
export default {
name: 'PollViewer',
components: {
NcActions,
NcActionButton,
NcActionLink,
NcCheckboxRadioSwitch,
NcLoadingIcon,
NcModal,
Expand All @@ -140,6 +157,7 @@ export default {
PollVotersDetails,
// icons
FileLock,
IconFileDownload,
IconFileEdit,
PollIcon,
},
Expand All @@ -159,6 +177,14 @@ export default {
const poll = computed(() => pollsStore.getPoll(token.value, id.value))
const supportPollDrafts = computed(() => hasTalkFeature(token.value, 'talk-polls-drafts'))
const exportPollURI = computed(() => convertToJSONDataURI({
question: poll.value.question,
options: poll.value.options,
resultMode: poll.value.resultMode,
maxVotes: poll.value.maxVotes,
}))
const exportPollFileName = `Talk Poll ${new Date().toISOString().slice(0, 10)}`
return {
isInCall: useIsInCall(),
pollsStore,
Expand All @@ -171,6 +197,8 @@ export default {
token,
poll,
supportPollDrafts,
exportPollURI,
exportPollFileName,
}
},
Expand Down
30 changes: 30 additions & 0 deletions src/utils/fileDownload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Converts given payload to Data URI scheme
* data:[<mediatype>][;base64],<data>
*
* @param payload data to convert
* @param mediatype a MIME type string
* @param base64Token an optional base64 token if non-textual data is given
*/
const convertToDataURI = function(payload: string, mediatype: string = 'text/plain;charset=US-ASCII', base64Token: string = ''): string {
return 'data:' + mediatype + base64Token + ',' + encodeURIComponent(payload)
}

/**
* Converts given JS object to Data URI scheme
*
* @param payload JS object to convert
*/
const convertToJSONDataURI = function(payload: object): string {
return convertToDataURI(JSON.stringify(payload, null, 2), 'application/json;charset=utf-8')
}

export {
convertToDataURI,
convertToJSONDataURI,
}
46 changes: 46 additions & 0 deletions src/utils/validatePollForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { createPollParams } from '../types/index.ts'

type requiredPollParams = Omit<createPollParams, 'draft'>
const pollFormExample = {
question: '',
options: ['', ''],
resultMode: 0,
maxVotes: 0,
}
const REQUIRED_KEYS: Array<keyof requiredPollParams> = Object.keys(pollFormExample) as Array<keyof requiredPollParams>

/**
* Parses a given JSON object and validates with required poll form object.
* Throws an error if parsed object doesn't match
* @param jsonObject The object to validate
*/
function validatePollForm(jsonObject: requiredPollParams): requiredPollParams {
if (typeof jsonObject !== 'object') {
throw new Error('Invalid parsed object')
}

for (const key of REQUIRED_KEYS) {
if (jsonObject[key] === undefined) {
throw new Error('Missing required key')
}

if (typeof pollFormExample[key] !== typeof jsonObject[key]) {
throw new Error('Invalid parsed value')
}

if (key === 'options' && jsonObject[key]?.some((opt: unknown) => typeof opt !== 'string')) {
throw new Error('Invalid parsed option values')
}
}

return jsonObject
}

export {
validatePollForm,
}

0 comments on commit ed6ad0d

Please sign in to comment.