diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-01-24 18:27:35 +0100 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2020-01-29 17:26:05 +0100 |
commit | 0ff05f50d458b5875707bea33ccf2b8976254edb (patch) | |
tree | d7e0c03b52eee2935508ceb848d7734d3e8b0f62 | |
parent | 503be8085b4e02e22bd69dbea959bbea7534cc76 (diff) |
Allow importing from files
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r-- | package-lock.json | 100 | ||||
-rw-r--r-- | package.json | 11 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAvatar.vue | 7 | ||||
-rw-r--r-- | src/components/Properties/PropertyDateTime.vue | 2 | ||||
-rw-r--r-- | src/components/Properties/PropertyMultipleText.vue | 4 | ||||
-rw-r--r-- | src/components/Settings/SettingsImportContacts.vue | 194 | ||||
-rw-r--r-- | src/components/SettingsSection.vue | 2 | ||||
-rw-r--r-- | src/main.js | 4 | ||||
-rw-r--r-- | src/models/rfcProps.js | 2 | ||||
-rw-r--r-- | src/router/index.js | 2 | ||||
-rw-r--r-- | src/services/cdav.js | 4 |
11 files changed, 257 insertions, 75 deletions
diff --git a/package-lock.json b/package-lock.json index b3cfe912..9cf2ee76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2573,6 +2573,21 @@ "core-js": "^3.5.0" } }, + "@nextcloud/dialogs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-1.0.0.tgz", + "integrity": "sha512-CV7sCJg37866j4ilNnf4PwvN2jNq6qZu+/cIdXLUZHUmK4HOfsSCqBmVqsf83Nq95VR6p/lCF1K1YbCvQG/P+g==", + "requires": { + "core-js": "3.5.0" + }, + "dependencies": { + "core-js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.5.0.tgz", + "integrity": "sha512-Ifh3kj78gzQ7NAoJXeTu+XwzDld0QRIwjBLRqAMhuLhP3d2Av5wmgE9ycfnvK6NAEjTkQ1sDPeoEZAWO3Hx1Uw==" + } + } + }, "@nextcloud/event-bus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.1.2.tgz", @@ -2590,6 +2605,51 @@ } } }, + "@nextcloud/initial-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-1.1.0.tgz", + "integrity": "sha512-c8VNSv7CbcPdaMNQO3ERJUMhsGyCvAgSBlvBHhugYHxGqlySjE+J+SqkpXmqB+eQ/DujDTahBX1IwoF3zjPtOw==", + "requires": { + "core-js": "3.6.1" + }, + "dependencies": { + "core-js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.1.tgz", + "integrity": "sha512-186WjSik2iTGfDjfdCZAxv2ormxtKgemjC3SI6PL31qOA0j5LhTDVjHChccoc7brwLvpvLPiMyRlcO88C4l1QQ==" + } + } + }, + "@nextcloud/l10n": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-1.0.1.tgz", + "integrity": "sha512-42Uh7vFTwCUNziRfwunrYUzsKxmulp4DH+RamB4DsFrYPH4z2qyQ4a2UBZJsZAcT1720rDagKQaXgPB2+Q0/0w==", + "requires": { + "core-js": "3.6.1" + }, + "dependencies": { + "core-js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.1.tgz", + "integrity": "sha512-186WjSik2iTGfDjfdCZAxv2ormxtKgemjC3SI6PL31qOA0j5LhTDVjHChccoc7brwLvpvLPiMyRlcO88C4l1QQ==" + } + } + }, + "@nextcloud/paths": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-1.1.0.tgz", + "integrity": "sha512-2fXdjY8cya5k38dIF7+BCYvJhprpWSsIGEiJDFKZQ4X02+jxP4IzI8qPG97iej7yhuQGN06xpmUv1OcT5NPftA==", + "requires": { + "core-js": "3.6.1" + }, + "dependencies": { + "core-js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.1.tgz", + "integrity": "sha512-186WjSik2iTGfDjfdCZAxv2ormxtKgemjC3SI6PL31qOA0j5LhTDVjHChccoc7brwLvpvLPiMyRlcO88C4l1QQ==" + } + } + }, "@nextcloud/router": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-1.0.0.tgz", @@ -9166,46 +9226,6 @@ "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", "dev": true }, - "nextcloud-auth": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/nextcloud-auth/-/nextcloud-auth-0.0.3.tgz", - "integrity": "sha512-qEAl55QJg2gZZIpfin9NzCPWm/Mfbo+HOdaXpsastPZw8oA7YLFFZon3x6SQ/p/LVIPQzRZmMpjd8R2FAAbjzg==", - "requires": { - "core-js": "^3.1.4" - } - }, - "nextcloud-dialogs": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/nextcloud-dialogs/-/nextcloud-dialogs-0.0.3.tgz", - "integrity": "sha512-lR6KsGU8IRPIYijjES0AaSKbEYoYAOnZHHqvH0dbwg4T00/n6poIzXlt392tESXKkO9Rxl/zz+eCs5U0PTYkGA==", - "requires": { - "core-js": "^3.1.4" - } - }, - "nextcloud-initial-state": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/nextcloud-initial-state/-/nextcloud-initial-state-0.0.3.tgz", - "integrity": "sha512-sL0dKbOb63QwvkAfQdDC5AldshVwaY8B8tKpAci7UMmJV3M1KLxTBzQoY+CVy03/uqTvvFt3Brz/Bd2UNp3zsQ==", - "requires": { - "core-js": "^3.1.4" - } - }, - "nextcloud-l10n": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/nextcloud-l10n/-/nextcloud-l10n-0.1.1.tgz", - "integrity": "sha512-3ftj9xyJjuqygRrVkxvNbFbOQHoGotMm/AJIpDUDwT91j4ICbgl2lif8Xdoc6oKJ1CtKgZ86eNlpn/vsCKIjfg==", - "requires": { - "core-js": "^3.1.4" - } - }, - "nextcloud-router": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/nextcloud-router/-/nextcloud-router-0.0.9.tgz", - "integrity": "sha512-w0i4xqFwJJuXNWFf9AB9huCWW5XmwdJHSHa7oXlOLTAvP9WxwU3KCm/mcKy8Eb0cT0ElRPg72HLUxl7oyEWoBQ==", - "requires": { - "core-js": "^3.1.4" - } - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index bb484e0a..a9639138 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,12 @@ "test:watch": "mochapack -w --webpack-config webpack.test.js --require tests/setup.js tests/unit/specs/**/*.spec.js" }, "dependencies": { + "@nextcloud/auth": "^1.2.1", + "@nextcloud/dialogs": "^1.0.0", + "@nextcloud/initial-state": "^1.1.0", + "@nextcloud/l10n": "^1.0.1", + "@nextcloud/paths": "^1.1.0", + "@nextcloud/router": "^1.0.0", "@nextcloud/vue": "1.2.7", "axios": "^0.19.1", "cdav-library": "github:nextcloud/cdav-library", @@ -43,11 +49,6 @@ "downloadjs": "^1.4.7", "ical.js": "^1.3.0", "moment": "^2.24.0", - "nextcloud-auth": "0.0.3", - "nextcloud-dialogs": "0.0.3", - "nextcloud-initial-state": "0.0.3", - "nextcloud-l10n": "0.1.1", - "nextcloud-router": "0.0.9", "p-limit": "^2.2.2", "p-queue": "^6.2.1", "qr-image": "^3.2.0", diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue index 703a3d4f..76433d41 100644 --- a/src/components/ContactDetails/ContactDetailsAvatar.vue +++ b/src/components/ContactDetails/ContactDetailsAvatar.vue @@ -88,8 +88,9 @@ import debounce from 'debounce' import { ActionLink, ActionButton } from '@nextcloud/vue' -import { getFilePickerBuilder } from 'nextcloud-dialogs' -import { generateRemoteUrl } from 'nextcloud-router' +import { getFilePickerBuilder } from '@nextcloud/dialogs' +import { generateRemoteUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' const axios = () => import('axios') @@ -113,7 +114,7 @@ export default { maximizeAvatar: false, opened: false, loading: false, - root: generateRemoteUrl(`dav/files/${OC.getCurrentUser().uid}`), + root: generateRemoteUrl(`dav/files/${getCurrentUser().uid}`), width: 0, height: 0, } diff --git a/src/components/Properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue index 03144509..cd0af107 100644 --- a/src/components/Properties/PropertyDateTime.vue +++ b/src/components/Properties/PropertyDateTime.vue @@ -85,7 +85,7 @@ import debounce from 'debounce' import moment from 'moment' import { DatetimePicker } from '@nextcloud/vue' -import { getLocale } from 'nextcloud-l10n' +import { getLocale } from '@nextcloud/l10n' import { VCardTime } from 'ical.js' import PropertyMixin from '../../mixins/PropertyMixin' diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue index cd4107e9..896ff661 100644 --- a/src/components/Properties/PropertyMultipleText.vue +++ b/src/components/Properties/PropertyMultipleText.vue @@ -63,8 +63,8 @@ @input="updateValue"> <!-- props actions --> - <PropertyActions class="property__actions--floating" - v-if="!isReadOnly" + <PropertyActions v-if="!isReadOnly" + class="property__actions--floating" :actions="actions" :property-component="this" @delete="deleteProperty" /> diff --git a/src/components/Settings/SettingsImportContacts.vue b/src/components/Settings/SettingsImportContacts.vue index d7583a3a..f0b6f04c 100644 --- a/src/components/Settings/SettingsImportContacts.vue +++ b/src/components/Settings/SettingsImportContacts.vue @@ -23,21 +23,56 @@ <template> <div class="import-contact"> <template v-if="!isNoAddressbookAvailable"> - <input id="contact-import" - :disabled="isImporting" - type="file" - class="hidden-visually" - @change="processFile"> - <label id="upload" for="contact-import" class="button import-contact__multiselect-label icon-upload"> - {{ isImporting ? t('contacts', 'Importing into') : t('contacts', 'Import into') }} - </label> - <multiselect - v-model="selectedAddressbook" - :options="options" - :disabled="isSingleAddressbook || isImporting" - :placeholder="t('contacts', 'Contacts')" - label="displayName" - class="import-contact__multiselect" /> + <button class="import-contact__button-main" @click="toggleModal"> + <span class="icon-upload" /> + {{ t('contacts', 'Import contacts') }} + </button> + <Modal v-if="isOpened" + ref="modal" + class="import-contact__modal" + :title="t('contacts', 'Import contacts')" + @close="toggleModal"> + <section class="import-contact__modal-addressbook"> + <h3>{{ t('contacts', 'Import contacts') }}</h3> + <multiselect + v-if="!isSingleAddressbook" + id="select-addressbook" + v-model="selectedAddressbook" + :allow-empty="false" + :options="options" + :disabled="isSingleAddressbook || isImporting" + :placeholder="t('contacts', 'Contacts')" + label="displayName" + class="import-contact__multiselect"> + <template slot="singleLabel" slot-scope="{ option }"> + {{ t('contacts', `Import into the '{addressbookName}' addressbook`, { addressbookName: option.displayName }) }} + </template> + </multiselect> + </section> + <section class="import-contact__modal-pick"> + <input id="contact-import" + ref="contact-import-input" + :disabled="loading || isImporting" + type="file" + class="hidden-visually" + @change="processFile"> + <button + :disabled="loading" + class="button import-contact__button import-contact__button--local" + @click="clickImportInput"> + <span class="import-contact__button-icon icon-upload" /> + {{ t('contacts', 'Select local file') }} + </button> + <button + :class="{'icon-loading': loading}" + :disabled="loading" + class="button primary import-contact__button import-contact__button--files" + @click="openPicker"> + <span class="import-contact__button-icon icon-folder-white" /> + {{ t('contacts', 'Import from files') }} + </button> + </section> + </Modal> </template> <button v-else id="upload" @@ -49,12 +84,31 @@ </template> <script> +import { encodePath } from '@nextcloud/paths' +import { getCurrentUser } from '@nextcloud/auth' +import { generateRemoteUrl } from '@nextcloud/router' +import { getFilePickerBuilder } from '@nextcloud/dialogs' +import axios from 'axios' + +const CancelToken = axios.CancelToken + +const picker = getFilePickerBuilder(t('contacts', 'Choose a vcard file to import')) + .setMultiSelect(false) + .setModal(true) + .setType(1) + .allowDirectories(false) + .setMimeTypeFilter('text/vcard') + .build() + export default { name: 'SettingsImportContacts', data() { return { + cancelRequest: () => {}, importDestination: false, + isOpened: false, + loading: false, } }, @@ -110,20 +164,126 @@ export default { }, }, methods: { + /** + * Process input type file change + * + * @param {Event} event the input change event + */ processFile(event) { + this.loading = true + this.$store.dispatch('changeStage', 'parsing') + const file = event.target.files[0] const reader = new FileReader() const selectedAddressbook = this.selectedAddressbook - this.$store.dispatch('changeStage', 'parsing') - this.$store.dispatch('setAddressbook', selectedAddressbook.displayName) + const self = this reader.onload = function(e) { + self.isOpened = false self.$store.dispatch('importContactsIntoAddressbook', { vcf: reader.result, addressbook: selectedAddressbook }) + // reset input event.target.value = '' + self.resetState() } reader.readAsText(file) }, + + toggleModal() { + this.isOpened = !this.isOpened + // cancel any ongoing request if closed + if (!this.isOpened) { + this.cancelRequest() + } + }, + + clickImportInput() { + this.$refs['contact-import-input'].click() + }, + + /** + * Open nextcloud file picker + */ + async openPicker() { + try { + this.loading = true + // unlikely, but let's cancel any previous request + this.cancelRequest() + + // prepare cancel token for axios request + const source = CancelToken.source() + this.cancelRequest = source.cancel + + // pick and retrieve file + const path = await picker.pick() + const file = await axios.get(generateRemoteUrl(`dav/files/${getCurrentUser().uid}`) + encodePath(path), { + cancelToken: source.token, + }) + + this.$store.dispatch('changeStage', 'parsing') + this.$store.dispatch('setAddressbook', this.selectedAddressbook.displayName) + + if (file.data) { + await this.$store.dispatch('importContactsIntoAddressbook', { vcf: file.data, addressbook: this.selectedAddressbook }) + } + } catch (error) { + console.error('Something wrong happened while picking a file', error) + } finally { + this.resetState() + } + }, + + /** + * Reset default component state + */ + resetState() { + this.cancelRequest = () => {} + this.importDestination = false + this.isOpened = false + this.loading = false + }, }, } </script> + +<style lang="scss" scoped> +.import-contact { + &__modal { + section { + padding: 22px; + // only one padding bewteen sections + &:not(:last-child) { + padding-bottom: 0; + } + } + &-pick { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-evenly; + } + } + &__button { + display: flex; + align-items: center; + flex: 0 1 150px; + width: 150px; + // spread evenly + margin: 10px; + padding: 10px; + &-icon { + width: 32px; + height: 32px; + margin-right: 5px; + } + &-main { + width: 100%; + } + &--cancel:not(:focus):not(:hover) { + border-color: transparent; + background-color: transparent; + } + } +} + +</style> diff --git a/src/components/SettingsSection.vue b/src/components/SettingsSection.vue index 7053356b..e0757716 100644 --- a/src/components/SettingsSection.vue +++ b/src/components/SettingsSection.vue @@ -26,11 +26,11 @@ <SettingsAddressbook v-for="addressbook in addressbooks" :key="addressbook.id" :addressbook="addressbook" /> </ul> <SettingsNewAddressbook :addressbooks="addressbooks" /> + <SettingsSortContacts class="settings-section" /> <SettingsImportContacts :addressbooks="addressbooks" class="settings-section" @clicked="onClickImport" @fileLoaded="onLoad" /> - <SettingsSortContacts class="settings-section" /> </div> </template> diff --git a/src/main.js b/src/main.js index 87e8c2db..47b4353e 100644 --- a/src/main.js +++ b/src/main.js @@ -28,8 +28,8 @@ import App from './ContactsRoot' import router from './router' import store from './store' import { sync } from 'vuex-router-sync' -import { generateFilePath } from 'nextcloud-router' -import { getRequestToken } from 'nextcloud-auth' +import { generateFilePath } from '@nextcloud/router' +import { getRequestToken } from '@nextcloud/auth' /** GLOBAL COMPONENTS AND DIRECTIVE */ import { Actions, DatetimePicker, Multiselect, PopoverMenu, Modal } from '@nextcloud/vue' diff --git a/src/models/rfcProps.js b/src/models/rfcProps.js index ca9b4e33..311f61f5 100644 --- a/src/models/rfcProps.js +++ b/src/models/rfcProps.js @@ -20,7 +20,7 @@ * */ import { VCardTime } from 'ical.js' -// import { loadState } from 'nextcloud-initial-state' +// import { loadState } from '@nextcloud/initial-state' import { loadState } from '../services/initialstate' import ActionCopyNtoFN from '../components/Actions/ActionCopyNtoFN' diff --git a/src/router/index.js b/src/router/index.js index 1f8fd221..eb2c9758 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -22,7 +22,7 @@ import Vue from 'vue' import Router from 'vue-router' -import { generateUrl } from 'nextcloud-router' +import { generateUrl } from '@nextcloud/router' import Contacts from '../views/Contacts' Vue.use(Router) diff --git a/src/services/cdav.js b/src/services/cdav.js index da5c6549..82ba3759 100644 --- a/src/services/cdav.js +++ b/src/services/cdav.js @@ -21,8 +21,8 @@ */ import DavClient from 'cdav-library' -import { generateRemoteUrl } from 'nextcloud-router' -import { getRequestToken } from 'nextcloud-auth' +import { generateRemoteUrl } from '@nextcloud/router' +import { getRequestToken } from '@nextcloud/auth' function xhrProvider() { const headers = { |