From 11691c0ed55664d0893f10d9e9d08ed6711513ab Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Fri, 18 Oct 2024 18:44:43 +0200 Subject: [PATCH 1/2] feat: allow to import poll from JSON file Signed-off-by: Maksim Sukharev --- .../NewMessage/NewMessagePollEditor.vue | 46 +++++++++++++++++++ src/utils/validatePollForm.ts | 46 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/utils/validatePollForm.ts diff --git a/src/components/NewMessage/NewMessagePollEditor.vue b/src/components/NewMessage/NewMessagePollEditor.vue index 008c912b936..2275c620425 100644 --- a/src/components/NewMessage/NewMessagePollEditor.vue +++ b/src/components/NewMessage/NewMessagePollEditor.vue @@ -24,6 +24,12 @@

+ + {{ t('spreed', 'Browse poll drafts') }} + + + {{ t('spreed', 'Import draft from file') }} +
@@ -96,8 +108,10 @@ 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 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' @@ -113,6 +127,7 @@ 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 { validatePollForm } from '../../utils/validatePollForm.ts' const props = defineProps<{ token: string, @@ -131,6 +146,7 @@ const pollsStore = usePollsStore() const isOpenedFromDraft = ref(false) const pollOption = ref(null) +const pollImport = ref(null) const pollForm = reactive({ question: '', @@ -206,6 +222,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 diff --git a/src/utils/validatePollForm.ts b/src/utils/validatePollForm.ts new file mode 100644 index 00000000000..26b4204b3aa --- /dev/null +++ b/src/utils/validatePollForm.ts @@ -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 +const pollFormExample = { + question: '', + options: ['', ''], + resultMode: 0, + maxVotes: 0, +} +const REQUIRED_KEYS: Array = Object.keys(pollFormExample) as Array + +/** + * 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, +} From f120568723e45b2571e1c99273b1b7540145353a Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Fri, 18 Oct 2024 18:11:04 +0200 Subject: [PATCH 2/2] feat: allow owners and moderators to export poll to JSON file Signed-off-by: Maksim Sukharev --- .../NewMessage/NewMessagePollEditor.vue | 13 ++++++ src/components/PollViewer/PollViewer.vue | 44 +++++++++++++++---- src/utils/fileDownload.ts | 30 +++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/utils/fileDownload.ts diff --git a/src/components/NewMessage/NewMessagePollEditor.vue b/src/components/NewMessage/NewMessagePollEditor.vue index 2275c620425..e7e63d45613 100644 --- a/src/components/NewMessage/NewMessagePollEditor.vue +++ b/src/components/NewMessage/NewMessagePollEditor.vue @@ -94,6 +94,12 @@ {{ t('spreed', 'Save as draft') }} + + + {{ t('spreed', 'Export draft to file') }} + {{ t('spreed', 'Create poll') }} @@ -107,6 +113,7 @@ 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' @@ -115,6 +122,7 @@ 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' @@ -127,6 +135,7 @@ 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<{ @@ -176,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 diff --git a/src/components/PollViewer/PollViewer.vue b/src/components/PollViewer/PollViewer.vue index a9db98079e7..d11db55e553 100644 --- a/src/components/PollViewer/PollViewer.vue +++ b/src/components/PollViewer/PollViewer.vue @@ -78,6 +78,12 @@ {{ t('spreed', 'Save as draft') }} + + + {{ t('spreed', 'Export draft to file') }} + {{ t('spreed', 'End poll') }}