summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2018-09-27 18:41:11 +0200
committerGitHub <noreply@github.com>2018-09-27 18:41:11 +0200
commit6d848dcd3bf513076bb2f5cadb138898422d4b30 (patch)
tree1d1a510c683afba5fa18bee50ddedc2d12bd4977
parent309dbc5efd6d87eb89a1b6c99f7b957bcc3e4513 (diff)
parent42953d1ed21cdff26c3f144a1013a1244066a2bf (diff)
Merge pull request #648 from nextcloud/vue-import-dav-fix
Fix import and push vCards to the server
-rw-r--r--css/importScreen.scss17
-rw-r--r--package-lock.json54
-rw-r--r--package.json5
-rw-r--r--src/components/ImportScreen.vue33
-rw-r--r--src/components/Settings/SettingsImportContacts.vue2
-rw-r--r--src/services/parseVcf.js6
-rw-r--r--src/store/addressbooks.js47
-rw-r--r--src/store/groups.js73
-rw-r--r--src/store/importState.js15
-rw-r--r--src/views/Contacts.vue2
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)
})
},