nextcloud-calendar/src/components/AppNavigation/Settings/SettingsImportSection.vue

277 lines
7.3 KiB
Vue

<!--
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
- @author Georg Ehrke <oc.list@georgehrke.com>
-
- @license GNU AGPL version 3 or any later version
-
- 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 <http://www.gnu.org/licenses/>.
-
-->
<template>
<li v-if="showProgressBar" class="settings-fieldset-interior-item">
<progress
class="settings-fieldset-interior-item__progressbar"
:value="imported"
:max="total" />
</li>
<li v-else class="settings-fieldset-interior-item">
<label class="settings-fieldset-interior-item__import-button button icon icon-upload" :for="inputUid">
{{ $n('calendar', 'Import calendar', 'Import calendars', 1) }}
</label>
<input
:id="inputUid"
ref="importInput"
class="hidden"
type="file"
:accept="supportedFileTypes"
:disabled="disableImport"
multiple
@change="processFiles">
<ImportScreen
v-if="showImportModal"
:files="files"
@cancel-import="cancelImport"
@import-calendar="importCalendar" />
</li>
</template>
<script>
import {
mapState,
} from 'vuex'
import { getParserManager } from 'calendar-js'
import ImportScreen from './ImportScreen.vue'
import { readFileAsText } from '../../../services/readFileAsTextService.js'
import {
showSuccess,
showWarning,
showError,
} from '@nextcloud/dialogs'
import {
IMPORT_STAGE_AWAITING_USER_SELECT,
IMPORT_STAGE_DEFAULT,
IMPORT_STAGE_IMPORTING,
IMPORT_STAGE_PROCESSING,
} from '../../../models/consts.js'
export default {
name: 'SettingsImportSection',
components: {
ImportScreen,
},
props: {
isDisabled: {
type: Boolean,
required: true,
},
},
computed: {
...mapState({
files: state => state.importFiles.importFiles,
stage: state => state.importState.stage,
total: state => state.importState.total,
accepted: state => state.importState.accepted,
denied: state => state.importState.denied,
}),
/**
* Total amount of processed calendar-objects, either accepted or failed
*
* @returns {Number}
*/
imported() {
return this.accepted + this.denied
},
/**
* Whether or not to display the upload button
*
* @returns {Boolean}
*/
allowUploadOfFiles() {
return this.stage === IMPORT_STAGE_DEFAULT
},
/**
* Whether or not to display the import modal
*
* @returns {Boolean}
*/
showImportModal() {
return this.stage === IMPORT_STAGE_AWAITING_USER_SELECT
},
/**
* Whether or not to display progress bar
*
* @returns {Boolean}
*/
showProgressBar() {
return this.stage === IMPORT_STAGE_IMPORTING
},
/**
* Unique identifier for the input field.
* Needed for the label
*
* @returns {String}
*/
inputUid() {
return this._uid + '-import-input'
},
/**
* Get a list of supported file-types for the file-picker
*
* This list comes straight from calendar-js.
* So in case we add new supported file-types there,
* we don't have to change anything here
*
* @returns {String[]}
*/
supportedFileTypes() {
return getParserManager().getAllSupportedFileTypes()
},
/**
* Whether or not the import button is disabled
*
* @returns {Boolean}
*/
disableImport() {
return this.isDisabled || !this.allowUploadOfFiles
},
},
methods: {
/**
* Process all files submitted from the user
*
* @param {Event} event The change-event of the input-field
*/
async processFiles(event) {
this.$store.commit('changeStage', IMPORT_STAGE_PROCESSING)
let addedFiles = false
for (const file of event.target.files) {
const contents = await readFileAsText(file)
const lastModified = file.lastModified
const name = file.name
const size = file.size
let type = file.type
// Handle cases where we are running inside a browser on Windows
//
// https://developer.mozilla.org/en-US/docs/Web/API/File/type
// "Uncommon" file-extensions will result in an empty type
// and apparently Microsoft considers calendar files to be "uncommon"
if (type === '') {
// If it's an xml file, our best guess is xCal: https://tools.ietf.org/html/rfc6321
// If it's a json file, our best guess is jCal: https://tools.ietf.org/html/rfc7265
// In every other case, our best guess is just plain old iCalendar: https://tools.ietf.org/html/rfc5545
if (name.endsWith('.xml')) {
type = 'application/calendar+xml'
} else if (name.endsWith('.json')) {
type = 'application/calendar+json'
} else if (name.endsWith('.csv')) {
type = 'text/csv'
} else {
type = 'text/calendar'
}
}
// Make sure the user didn't select
// files of a different file-type
if (!this.supportedFileTypes.includes(type)) {
showError(this.$t('calendar', '{filename} is an unsupported file-type', {
filename: name,
}))
continue
}
// Use custom-options for parser.
// The last one in particular will prevent thousands
// of invitation emails to be sent out on import
const parser = getParserManager().getParserForFileType(type, {
extractGlobalProperties: true,
includeTimezones: true,
removeRSVPForAttendees: true,
})
try {
parser.parse(contents)
} catch (error) {
console.error(error)
showError(this.$t('calendar', '{filename} could not be parsed', {
filename: name,
}))
continue
}
this.$store.commit('addFile', {
contents,
lastModified,
name,
size,
type,
parser,
})
addedFiles = true
}
if (!addedFiles) {
showError(this.$t('calendar', 'No valid files found, aborting import'))
this.$store.commit('removeAllFiles')
this.$store.commit('resetState')
return
}
this.$store.commit('changeStage', IMPORT_STAGE_AWAITING_USER_SELECT)
},
/**
* Import all events into the calendars
* This will show
*/
async importCalendar() {
await this.$store.dispatch('importEventsIntoCalendar')
if (this.total === this.accepted) {
showSuccess(this.$n('calendar', 'Successfully imported %n event', 'Successfully imported %n events.', this.total))
} else {
showWarning(this.$t('calendar', 'Import partially failed. Imported {accepted} out of {total}.', {
accepted: this.accepted,
total: this.total,
}))
}
this.$store.commit('removeAllFiles')
this.$store.commit('resetState')
// Once we are done importing, reload the calendar view
this.$store.commit('incrementModificationCount')
this.resetInput()
},
/**
* Resets the import sate
*/
cancelImport() {
this.$store.commit('removeAllFiles')
this.$store.commit('resetState')
this.resetInput()
},
/**
* Manually reset the file-input, because when you try to upload
* the exact same files again, it won't trigger the change event
*/
resetInput() {
this.$refs.importInput.value = ''
},
},
}
</script>