diff --git a/src/components/Editor/Attachments/AttachmentsList.vue b/src/components/Editor/Attachments/AttachmentsList.vue new file mode 100644 index 0000000000..17686be3bb --- /dev/null +++ b/src/components/Editor/Attachments/AttachmentsList.vue @@ -0,0 +1,222 @@ + + + + + + + + + + + {{ attachment.formatType }} + + + + + + + {{ t('calendar', 'View file') }} + + + + + + {{ t('calendar', 'Delete file') }} + + + + + + + + + + + {{ t('calendar', 'Choose') }} + + + + + + {{ t('calendar', 'Upload') }} + + + + + + + + + {{ t('calendar', 'This event has no attachments.') }} + + + + + + {{ t('calendar', 'Choose') }} + + + + + + {{ t('calendar', 'Upload') }} + + + + + + + + + + diff --git a/src/models/attachment.js b/src/models/attachment.js new file mode 100644 index 0000000000..7b0e37e798 --- /dev/null +++ b/src/models/attachment.js @@ -0,0 +1,61 @@ +/** + * @copyright Copyright (c) 2022 Mikhail Sazanov + * + * @author Mikhail Sazanov + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Creates a complete attachment object based on given props + * + * @param {object} props The attachment properties already provided + * @return {object} + */ +const getDefaultAttachmentObject = (props = {}) => Object.assign({}, { + // The calendar-js attachment property + attachmentProperty: null, + // The file name of the attachment + fileName: null, + // The attachment mime type + formatType: null, + // The uri of the attachment + uri: null, + // The value from calendar object + value: null, +}, props) + +/** + * Maps a calendar-js attachment property to our attachment object + * + * @param {attachmentProperty} attachmentProperty The calendar-js attachmentProperty to turn into a attachment object + * @return {object} + */ +const mapAttachmentPropertyToAttchmentObject = (attachmentProperty) => { + return getDefaultAttachmentObject({ + attachmentProperty, + fileName: typeof attachmentProperty._parameters.get('FILENAME') !== 'undefined' ? attachmentProperty._parameters.get('FILENAME').value : null, + formatType: attachmentProperty.formatType, + uri: attachmentProperty.uri, + value: attachmentProperty.value, + }) +} + +export { + getDefaultAttachmentObject, + mapAttachmentPropertyToAttchmentObject, +} diff --git a/src/models/event.js b/src/models/event.js index f72bc63fd4..52bcee2fe6 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -25,6 +25,7 @@ import { DurationValue } from '@nextcloud/calendar-js' import { getHexForColorName } from '../utils/color.js' import { mapAlarmComponentToAlarmObject } from './alarm.js' import { mapAttendeePropertyToAttendeeObject } from './attendee.js' +import { mapAttachmentPropertyToAttchmentObject } from './attachment.js' import { getDefaultRecurrenceRuleObject, mapRecurrenceRuleValueToRecurrenceRuleObject, @@ -85,6 +86,8 @@ const getDefaultEventObject = (props = {}) => Object.assign({}, { customColor: null, // Categories categories: [], + // Attachments of this event + attachments: [], }, props) /** @@ -154,6 +157,14 @@ const mapEventComponentToEventObject = (eventComponent) => { eventObject.attendees.push(mapAttendeePropertyToAttendeeObject(attendee)) } + /** + * Extract attachments + */ + + for (const attachment of eventComponent.getPropertyIterator('ATTACH')) { + eventObject.attachments.push(mapAttachmentPropertyToAttchmentObject(attachment)) + } + /** * Extract recurrence-rule */ diff --git a/src/services/attachmentService.js b/src/services/attachmentService.js new file mode 100644 index 0000000000..2bdfbad165 --- /dev/null +++ b/src/services/attachmentService.js @@ -0,0 +1,96 @@ +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' +import { showError, showSuccess } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' +import { async } from 'regenerator-runtime' + +/** + * Makes a share link for a given file or directory. + * @param {string} path The file path from the user's root directory. e.g. `/myfile.txt` + * @param {string} token The conversation's token + * @returns {string} url share link + */ +const shareFile = async function(path, token) { + try { + const res = await axios.post(generateOcsUrl('apps/files_sharing/api/v1/', 2) + 'shares', { + shareType: 3, // OC.Share.SHARE_TYPE_LINK, + path, + shareWith: token, + }) + return res.data.ocs.data + } catch (error) { + if ( + error.response + && error.response.data + && error.response.data.ocs + && error.response.data.ocs.meta + && error.response.data.ocs.meta.message + ) { + console.error(`Error while sharing file: ${error.response.data.ocs.meta.message || 'Unknown error'}`) + showError(error.response.data.ocs.meta.message) + throw error + } else { + console.error('Error while sharing file: Unknown error') + showError(t('mail', 'Error while sharing file')) + throw error + } + } +} + +const shareFileWith = async function(path, token, sharedWith, permissions = 17){ + try { + const res = await axios.post(generateOcsUrl('apps/files_sharing/api/v1/', 2) + 'shares', { + password: null, + shareType: 0, // WITH USERS, + permissions: permissions, // 14 - edit, 17 - view + path, + shareWith: sharedWith, + }) + return res.data.ocs.data + } catch (error) { + console.error(error) + } +} + +const uploadLocalAttachment = async function(event, dav, componentAttachments) { + const files = event.target.files + let attachments = []; + let promises = []; + + files.forEach(file => { + if(componentAttachments.map(attachment => attachment.fileName).indexOf(file.name) !== -1){ + showError(t('calendar', 'Attachment {fileName} already exists!',{ + fileName: file.name + })) + } + else { + const url = `/remote.php/dav/files/${dav.userId}/${file.name}` + const res = axios.put(url, file).then(resp => { + const data = { + fileName: file.name, + formatType: file.type, + uri: url, + value: url, + path: `/${file.name}` + } + if(resp.status === 204 || resp.status === 201){ + showSuccess(t('calendar', 'Attachment {fileName} added!',{ + fileName: file.name + })) + attachments.push(data) + } + }) + promises.push(res) + } + + }) + await Promise.all(promises) + return attachments + +} + +export { + shareFile, + shareFileWith, + uploadLocalAttachment +} diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js index a4e584f7e5..82a1ab7e9a 100644 --- a/src/store/calendarObjectInstance.js +++ b/src/store/calendarObjectInstance.js @@ -24,7 +24,7 @@ import getTimezoneManager from '../services/timezoneDataProviderService' import { getDateFromDateTimeValue, } from '../utils/date.js' -import { AttendeeProperty, Property, DateTimeValue, DurationValue, RecurValue } from '@nextcloud/calendar-js' +import { AttendeeProperty, Property, DateTimeValue, DurationValue, RecurValue, AttachmentProperty, Parameter } from '@nextcloud/calendar-js' import { getBySetPositionAndBySetFromDate, getWeekDayFromDate } from '../utils/recurrence.js' import { getDefaultEventObject, @@ -1350,6 +1350,65 @@ const mutations = { } } }, + + addAttachmentBySharedData(state, { calendarObjectInstance, sharedData }){ + const attachment = AttachmentProperty.fromLink(sharedData.url) + const fileName = sharedData.file_target.replace(/\//ig, "") + + // hot-fix needed temporary, becase calendar-js has no fileName get-setter + const parameterFileName = new Parameter('FILENAME', fileName) + attachment.setParameter(parameterFileName); + attachment.fileName = fileName + attachment.formatType = sharedData.mimetype + attachment.uri = sharedData.url + + calendarObjectInstance.eventComponent.addProperty(attachment) + calendarObjectInstance.attachments.push(attachment) + console.log('addAttachmentBySharedData', attachment) + }, + + addAttachmentByLocalFile(state, { calendarObjectInstance, file }){ + + const attachment = AttachmentProperty.fromLink(file.url) + // hot-fix needed temporary, becase calendar-js has no fileName get-setter + const parameterFileName = new Parameter('FILENAME', file.fileName) + attachment.setParameter(parameterFileName); + + attachment.fileName = file.fileName + attachment.formatType = file.formatType + attachment.uri = file.uri + + calendarObjectInstance.eventComponent.addProperty(attachment) + + file.attachmentProperty = attachment + calendarObjectInstance.attachments.push(file) + + console.log('addAttachmentByLocalFile', attachment) + }, + + + /** + * + * @param {object} state The Vuex state + * @param {object} data The destructuring object + * @param {object} data.calendarObjectInstance The calendarObjectInstance object + * @param {object} data.attachment The attachment object + */ + deleteAttachment(state, { calendarObjectInstance, attachment }) { + //todo - check this property + console.log(typeof attachment) + try { + const index = calendarObjectInstance.attachments.indexOf(attachment) + if (index !== -1) { + calendarObjectInstance.attachments.splice(index, 1) + } + calendarObjectInstance.eventComponent.removeAttachment(attachment.attachmentProperty) + } + catch(error){ + console.log(error) + } + + }, } const getters = {} diff --git a/src/views/EditSidebar.vue b/src/views/EditSidebar.vue index 7c6a28e8f2..07750e7a23 100644 --- a/src/views/EditSidebar.vue +++ b/src/views/EditSidebar.vue @@ -37,7 +37,6 @@ - {{ $t('calendar', 'Event does not exist') }} @@ -198,11 +197,32 @@ @save-this-only="saveAndLeave(false)" @save-this-and-all-future="saveAndLeave(true)" /> + + + + + + + + + + :order="3"> @@ -247,6 +267,7 @@ import PropertySelectMultiple from '../components/Editor/Properties/PropertySele import PropertyColor from '../components/Editor/Properties/PropertyColor.vue' import ResourceList from '../components/Editor/Resources/ResourceList' import InvitationResponseButtons from '../components/Editor/InvitationResponseButtons' +import AttachmentsList from '../components/Editor/Attachments/AttachmentsList.vue' import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue' import CalendarBlank from 'vue-material-design-icons/CalendarBlank.vue' @@ -254,6 +275,7 @@ import Delete from 'vue-material-design-icons/Delete.vue' import Download from 'vue-material-design-icons/Download.vue' import InformationOutline from 'vue-material-design-icons/InformationOutline.vue' import MapMarker from 'vue-material-design-icons/MapMarker.vue' +import Paperclip from 'vue-material-design-icons/Paperclip.vue' export default { name: 'EditSidebar', @@ -282,6 +304,8 @@ export default { InformationOutline, MapMarker, InvitationResponseButtons, + Paperclip, + AttachmentsList, }, mixins: [ EditorMixin, @@ -310,6 +334,9 @@ export default { return moment(this.calendarObjectInstance.startDate).locale(this.locale).fromNow() }, + attachments(){ + return this.calendarObjectInstance?.attachments || null + }, /** * @return {boolean} */ @@ -399,6 +426,7 @@ export default { customColor, }) }, + }, }
{{ t('calendar', 'This event has no attachments.') }}