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 @@
+
+
@@ -31,6 +37,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') }}
@@ -86,13 +92,21 @@
-
-
-
-
-
- {{ t('spreed', 'Save as draft') }}
-
+
+
+
+
+
+
+ {{ t('spreed', 'Save as draft') }}
+
+
+
+
+
+ {{ t('spreed', 'Export draft to file') }}
+
+
@@ -102,14 +116,15 @@