diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2018-09-27 18:41:11 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-09-27 18:41:11 +0200 |
commit | 6d848dcd3bf513076bb2f5cadb138898422d4b30 (patch) | |
tree | 1d1a510c683afba5fa18bee50ddedc2d12bd4977 | |
parent | 309dbc5efd6d87eb89a1b6c99f7b957bcc3e4513 (diff) | |
parent | 42953d1ed21cdff26c3f144a1013a1244066a2bf (diff) |
Merge pull request #648 from nextcloud/vue-import-dav-fix
Fix import and push vCards to the server
-rw-r--r-- | css/importScreen.scss | 17 | ||||
-rw-r--r-- | package-lock.json | 54 | ||||
-rw-r--r-- | package.json | 5 | ||||
-rw-r--r-- | src/components/ImportScreen.vue | 33 | ||||
-rw-r--r-- | src/components/Settings/SettingsImportContacts.vue | 2 | ||||
-rw-r--r-- | src/services/parseVcf.js | 6 | ||||
-rw-r--r-- | src/store/addressbooks.js | 47 | ||||
-rw-r--r-- | src/store/groups.js | 73 | ||||
-rw-r--r-- | src/store/importState.js | 15 | ||||
-rw-r--r-- | src/views/Contacts.vue | 2 |
10 files changed, 168 insertions, 86 deletions
diff --git a/css/importScreen.scss b/css/importScreen.scss index c57dca90..3d815389 100644 --- a/css/importScreen.scss +++ b/css/importScreen.scss @@ -25,13 +25,16 @@ padding-top: 20px; } &__progress { - width: 50%; - position: absolute; - left: 25%; + width: 50%; + min-width: 300px; + margin: auto; } &__tracker { - padding-top: 10px; - position: absolute; - left: 25%; + display: flex; + justify-content: space-between; + width: 50%; + min-width: 300px; + margin: auto; + padding-top: 10px; } -}
\ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index f5c2f5d8..9a03c67c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4014,10 +4014,13 @@ "from": "github:nextcloud/cdav-library#develop", "requires": { "compare-urls": "^2.0.0", - "davclient.js": "git+https://github.com/owncloud/davclient.js.git#0e5ef1af5d174d9ec10dbe889a415b8f481d5094", "normalize-url": "^3.3.0" }, "dependencies": { + "davclient.js": { + "version": "git+https://github.com/owncloud/davclient.js.git#0e5ef1af5d174d9ec10dbe889a415b8f481d5094", + "from": "git+https://github.com/owncloud/davclient.js.git#0e5ef1af5d174d9ec10dbe889a415b8f481d5094" + }, "normalize-url": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", @@ -4900,10 +4903,6 @@ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", "dev": true }, - "davclient.js": { - "version": "git+https://github.com/owncloud/davclient.js.git#0e5ef1af5d174d9ec10dbe889a415b8f481d5094", - "from": "git+https://github.com/owncloud/davclient.js.git" - }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -10349,9 +10348,9 @@ "dev": true }, "nextcloud-vue": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.1.4.tgz", - "integrity": "sha512-agkxQofRqVgCTOZ07Ndg/e6wGnDdP4AitXF76t7rw3WXnJpH/lEc5RgXc8T0wvpe+Z704dQHHZ2RtjYBHhcJHQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.1.5.tgz", + "integrity": "sha512-2tFfPPzhTMtZnbBmUk91o2o+jiri3X6BEgNs+iAWf9WZq4Gcpb6kIFW2ckizZuPFccmV1rA4Ts18IpU25vGERw==", "requires": { "@babel/polyfill": "^7.0.0", "v-tooltip": "^2.0.0-rc.33", @@ -10869,12 +10868,11 @@ "dev": true }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", + "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { @@ -10884,13 +10882,29 @@ "dev": true, "requires": { "p-limit": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" }, "pako": { "version": "1.0.6", @@ -15209,9 +15223,9 @@ "dev": true }, "webpack": { - "version": "4.20.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.20.1.tgz", - "integrity": "sha512-f2USD1msiBL2/0SWllYK9mVmIvt7NpeJYho/jucqEreGos5NjcdHdeDLB+VGDD0RuOjxL3hGUQlk2rWfB3yGkg==", + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.20.2.tgz", + "integrity": "sha512-75WFUMblcWYcocjSLlXCb71QuGyH7egdBZu50FtBGl2Nso8CK3Ej+J7bTZz2FPFq5l6fzCisD9modB7t30ikuA==", "dev": true, "requires": { "@webassemblyjs/ast": "1.7.8", diff --git a/package.json b/package.json index ca5646c8..425913f8 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "debounce": "^1.2.0", "ical.js": "^1.2.2", "moment": "^2.22.2", - "nextcloud-vue": "^0.1.3", + "nextcloud-vue": "^0.1.5", + "p-limit": "^2.0.0", "uuid": "^3.3.2", "v-tooltip": "^2.0.0-rc.33", "vue": "^2.5.17", @@ -72,7 +73,7 @@ "vue-jest": "^2.6.0", "vue-loader": "^15.4.2", "vue-template-compiler": "^2.5.17", - "webpack": "^4.20.1", + "webpack": "^4.20.2", "webpack-cli": "^3.1.1", "webpack-merge": "^4.1.4" }, diff --git a/src/components/ImportScreen.vue b/src/components/ImportScreen.vue index bb903442..b58182fa 100644 --- a/src/components/ImportScreen.vue +++ b/src/components/ImportScreen.vue @@ -23,19 +23,46 @@ <template> <div class="emptycontent import-screen"> <p class="icon-upload" /> - <h3 class="import-screen__header">{{ t('contacts', 'Importing into') }} {{ importState.addressbook }}</h3> - <progress :max="importState.total" :value="importState.accepted" class="import-screen__progress" /> - <p class="import-screen__tracker">{{ Math.floor(importState.accepted/(importState.total + 1)) * 100 }} %</p> + <h3 class="import-screen__header">{{ t('contacts', 'Importing {total} contacts into', { total }) }} {{ addressbook }}</h3> + <progress :max="total" :value="progress" class="import-screen__progress" /> + <p class="import-screen__tracker"> + <span>{{ percentage }} %</span> + <span v-tooltip.auto="t('contacts', 'Open your browser console for more details')">{{ denied }} {{ t('contacts', 'failed') }}</span> + </p> </div> </template> <script> +import Vue from 'vue' +import VTooltip from 'v-tooltip' + +Vue.use(VTooltip) export default { name: 'ImportScreen', computed: { importState() { return this.$store.getters.getImportState + }, + addressbook() { + return this.importState.addressbook + }, + total() { + return this.importState.total + }, + accepted() { + return this.importState.accepted + }, + denied() { + return this.importState.denied + }, + progress() { + return this.accepted + this.denied + }, + percentage() { + return this.total <= 0 + ? 0 + : Math.floor(this.progress / this.total * 100) } } } diff --git a/src/components/Settings/SettingsImportContacts.vue b/src/components/Settings/SettingsImportContacts.vue index 881308ae..d7a17044 100644 --- a/src/components/Settings/SettingsImportContacts.vue +++ b/src/components/Settings/SettingsImportContacts.vue @@ -98,6 +98,8 @@ export default { let self = this reader.onload = function(e) { self.$store.dispatch('importContactsIntoAddressbook', { vcf: reader.result, addressbook: selectedAddressbook }) + // reset input + event.target.value = '' } reader.readAsText(file) } diff --git a/src/services/parseVcf.js b/src/services/parseVcf.js index 65d5f95c..11b1fc49 100644 --- a/src/services/parseVcf.js +++ b/src/services/parseVcf.js @@ -26,14 +26,13 @@ import Store from '../store/index' export default function parseVcf(data = '', addressbook) { let regexp = /BEGIN:VCARD[\s\S]*?END:VCARD/mgi let vCards = data.match(regexp) - let importState = Store.getters.getImportState if (!vCards) { console.debug('Error during the parsing of the following vcf file: ', data) return [] } - importState.total = vCards.length + Store.dispatch('setTotal', vCards.length) // Not using map because we want to only push valid contacts // map force to return at least undefined @@ -41,11 +40,10 @@ export default function parseVcf(data = '', addressbook) { try { // console.log(vCards.indexOf(vCard)) let contact = new Contact(vCard, addressbook) - importState.accepted++ contacts.push(contact) } catch (e) { // Parse error! Do not stop here... - importState.denied++ + Store.dispatch('incrementDenied') console.error(e) } return contacts diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js index 74a47bef..d6ca8a1d 100644 --- a/src/store/addressbooks.js +++ b/src/store/addressbooks.js @@ -22,9 +22,11 @@ */ import Vue from 'vue' +import ICAL from 'ical.js' import parseVcf from '../services/parseVcf' import client from '../services/cdav' import Contact from '../models/contact' +import pLimit from 'p-limit' const addressbookModel = { id: '', @@ -298,7 +300,7 @@ const actions = { * @returns {Promise} */ async getContactsFromAddressBook(context, { addressbook }) { - return addressbook.dav.findAllAndFilterBySimpleProperties(['EMAIL', 'UID', 'CATEGORIES', 'FN', 'ORG']) + return addressbook.dav.findAllAndFilterBySimpleProperties(['EMAIL', 'UID', 'CATEGORIES', 'FN', 'ORG', 'N']) .then((response) => { // We don't want to lose the url information // so we need to parse one by one @@ -309,7 +311,7 @@ const actions = { }) context.commit('appendContactsToAddressbook', { addressbook, contacts }) context.commit('appendContacts', contacts) - context.commit('appendGroupsFromContacts', contacts) + context.commit('extractGroupsFromContacts', contacts) context.commit('sortContacts') return contacts }) @@ -327,15 +329,42 @@ const actions = { * @param {Object} context the store mutations * @param {Object} importDetails = { vcf, addressbook } */ - importContactsIntoAddressbook(context, { vcf, addressbook }) { - let contacts = parseVcf(vcf, addressbook) + async importContactsIntoAddressbook(context, { vcf, addressbook }) { + const contacts = parseVcf(vcf, addressbook) context.commit('changeStage', 'importing') - contacts.forEach(contact => { - context.commit('addContact', contact) - context.commit('addContactToAddressbook', contact) - context.commit('appendGroupsFromContacts', [contact]) + + // max simultaneous requests + const limit = pLimit(3) + const requests = [] + + // create the array of requests to send + contacts.map(async contact => { + // Get vcard string + try { + let vData = ICAL.stringify(contact.vCard.jCal) + // push contact to server and use limit + requests.push(limit(() => contact.addressbook.dav.createVCard(vData) + .then((response) => { + // success, update store + context.commit('addContact', contact) + context.commit('addContactToAddressbook', contact) + context.commit('extractGroupsFromContacts', [contact]) + context.commit('incrementAccepted') + }) + .catch((error) => { + // error + context.commit('incrementDenied') + console.error(error) + }) + )) + } catch (e) { + context.commit('incrementDenied') + } + }) + + Promise.all(requests).then(() => { + context.commit('changeStage', 'default') }) - context.commit('changeStage', 'default') }, /** diff --git a/src/store/groups.js b/src/store/groups.js index abd4599f..dd7a2393 100644 --- a/src/store/groups.js +++ b/src/store/groups.js @@ -27,40 +27,29 @@ const state = { const mutations = { /** * Extract all the groups from the provided contacts + * and add the contacts to their respective groups * * @param {Object} state the store data * @param {Contact[]} contacts the contacts to add - * TODO: create single contact mutation */ - appendGroupsFromContacts(state, contacts) { - // init groups list - let groups = Object.values(contacts) - // iterate on every contacts - .reduce((groups, contact) => { - // transform group names into Object - contact.groups.map(groupName => { - // overriding existing groups: remove duplicates - groups[groupName] = { - name: groupName, - contacts: [] + extractGroupsFromContacts(state, contacts) { + // iterate contacts + contacts.forEach(contact => { + if (contact.groups) { + contact.groups.forEach(groupName => { + let group = state.groups.find(search => search.name === groupName) + // nothing? create a new one + if (!group) { + state.groups.push({ + name: groupName, + contacts: [] + }) + group = state.groups.find(search => search.name === groupName) } + group.contacts.push(contact.key) }) - return groups - }, {}) - - // store in state - state.groups = Object.values(groups) - - // append keys to groups - Object.values(contacts) - .forEach(contact => { - if (contact.groups) { - contact.groups.forEach(groupName => { - let group = state.groups.find(search => search.name === groupName) - group.contacts.push(contact.key) - }) - } - }) + } + }) }, /** @@ -68,20 +57,22 @@ const mutations = { * * @param {Object} state the store data * @param {Object} data destructuring object - * @param {String} data.groupName the name of the group + * @param {Array<string>} data.groupNames the names of the group * @param {Contact} data.contact the contact */ - addContactToGroup(state, { groupName, contact }) { - let group = state.groups.find(search => search.name === groupName) - // nothing? create a new one - if (!group) { - state.groups.push({ - name: groupName, - contacts: [] - }) - group = state.groups.find(search => search.name === groupName) - } - group.contacts.push(contact.key) + addContactToGroups(state, { groupNames, contact }) { + groupNames.forEach(groupName => { + let group = state.groups.find(search => search.name === groupName) + // nothing? create a new one + if (!group) { + state.groups.push({ + name: groupName, + contacts: [] + }) + group = state.groups.find(search => search.name === groupName) + } + group.contacts.push(contact.key) + }) }, /** @@ -116,7 +107,7 @@ const actions = { * @param {Contact} data.contact the contact */ addContactToGroup(context, { groupName, contact }) { - context.commit('addContactToGroup', { groupName, contact }) + context.commit('addContactToGroups', { groupNames: [groupName], contact }) }, /** diff --git a/src/store/importState.js b/src/store/importState.js index 024b0ac0..b5adafc7 100644 --- a/src/store/importState.js +++ b/src/store/importState.js @@ -77,6 +77,17 @@ const mutations = { */ changeStage(state, stage) { state.importState.stage = stage + }, + + /** + * Reset to the default state + * + * @param {Object} state the store data + */ + resetState(state) { + state.importState.total = 0 + state.importState.accepted = 0 + state.importState.denied = 0 } } @@ -125,12 +136,16 @@ const actions = { /** * Change stage to the indicated one + * and reset if the parsing starts * * @param {Object} context the store mutations * @param {String} stage the name of the stage ('default', 'importing', 'parsing') */ changeStage(context, stage) { context.commit('changeStage', stage) + if (stage === 'parsing') { + context.commit('resetState') + } } } diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue index e9c236f2..fe947279 100644 --- a/src/views/Contacts.vue +++ b/src/views/Contacts.vue @@ -146,6 +146,8 @@ export default { counter: group.contacts.length } } + }).sort(function(a, b) { + return parseInt(b.utils.counter) - parseInt(a.utils.counter) }) }, |