diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-08-28 14:53:19 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-08-28 14:53:19 +0200 |
commit | 670493bd8a029d8e3b6ab75778cb2fd0cb16378a (patch) | |
tree | 2745cc445e21da851f20337cea12ee98fbf1db4c /src | |
parent | 0b5f975b5288e1a9548260f9ac8f47453982f9a7 (diff) |
Dav lib 1
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/ContactDetails.vue | 123 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsProperty.vue | 16 | ||||
-rw-r--r-- | src/components/Properties/PropertyDateTime.vue (renamed from src/components/properties/PropertyDateTime.vue) | 19 | ||||
-rw-r--r-- | src/components/Properties/PropertyGroups.vue (renamed from src/components/properties/PropertyGroups.vue) | 22 | ||||
-rw-r--r-- | src/components/Properties/PropertyMultipleText.vue (renamed from src/components/properties/PropertyMultipleText.vue) | 19 | ||||
-rw-r--r-- | src/components/Properties/PropertySelect.vue | 147 | ||||
-rw-r--r-- | src/components/Properties/PropertyText.vue (renamed from src/components/properties/PropertyText.vue) | 22 | ||||
-rw-r--r-- | src/components/Properties/PropertyTitle.vue (renamed from src/components/properties/PropertyTitle.vue) | 6 | ||||
-rw-r--r-- | src/models/contact.js | 11 | ||||
-rw-r--r-- | src/models/rfcProps.js | 17 | ||||
-rw-r--r-- | src/services/cdav.js | 47 | ||||
-rw-r--r-- | src/store/addressbooks.js | 70 | ||||
-rw-r--r-- | src/store/contacts.js | 20 | ||||
-rw-r--r-- | src/views/Contacts.vue | 67 |
14 files changed, 483 insertions, 123 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 95de110d..cae7cd9c 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -80,26 +80,33 @@ <!-- contact details --> <section class="contact-details"> + + <!-- properties iteration --> <contact-details-property v-for="(property, index) in sortedProperties" :key="index" :index="index" :sorted-properties="sortedProperties" :property="property" :contact="contact" @updatedcontact="updateContact" /> + + <!-- addressbook change select --> + <property-select :prop-model="addressbookModel" :value.sync="addressbook" + :options="addressbooksOptions" class="property--addressbooks" /> </section> </template> </div> </template> <script> - -import ICAL from 'ical.js' import ClickOutside from 'vue-click-outside' import Vue from 'vue' import VTooltip from 'v-tooltip' import debounce from 'debounce' + import Contact from '../models/contact' import rfcProps from '../models/rfcProps.js' -import popoverMenu from './core/popoverMenu' -import contactDetailsProperty from './ContactDetails/ContactDetailsProperty' +import PopoverMenu from './core/popoverMenu' +import ContactDetailsProperty from './ContactDetails/ContactDetailsProperty' +import PropertySelect from './Properties/PropertySelect' +import PropertyGroups from './Properties/PropertyGroups' Vue.use(VTooltip) @@ -107,8 +114,10 @@ export default { name: 'ContactDetails', components: { - popoverMenu, - contactDetailsProperty + PopoverMenu, + ContactDetailsProperty, + PropertySelect, + PropertyGroups }, directives: { @@ -128,12 +137,18 @@ export default { data() { return { - contact: undefined, - openedMenu: false + openedMenu: false, + addressbookModel: { + readableName: t('contacts', 'Addressbook'), + icon: 'icon-addressbook' + } } }, computed: { + /** + * Contact color based on uid + */ colorAvatar() { try { let color = this.contact.uid.toRgb() @@ -170,36 +185,59 @@ export default { */ sortedProperties() { return this.contact.properties.slice(0).sort((a, b) => { - return rfcProps.fieldOrder.indexOf(a.name) - rfcProps.fieldOrder.indexOf(b.name) + return ( + rfcProps.fieldOrder.indexOf(a.name) - + rfcProps.fieldOrder.indexOf(b.name) + ) }) - } - }, + }, + + // usable addressbook object linked to the local contact + addressbook: { + get: function() { + return { + id: this.contact.addressbook.id, + name: this.contact.addressbook.displayName + } + }, + set: function(addressbook) { + this.moveContactToAddressbook(addressbook) + } + }, - watch: { - // url changed, get and show selected contact - uid: function() { - this.updateLocalContact() + // store getters filtered and mapped to usable object + addressbooksOptions() { + return this.addressbooks + .filter(addressbook => addressbook.enabled) + .map(addressbook => { + return { + id: addressbook.id, + name: addressbook.displayName + } + }) }, - // done loading, check if the provided uid is valid and open details - loading: function() { - if (this.uid) { - this.updateLocalContact() + + // store getter + addressbooks() { + return this.$store.getters.getAddressbooks + }, + + // local version of the contact + contact() { + let contact = this.$store.getters.getContact(this.uid) + if (contact) { + // create empty contact and copy inner data + let localContact = new Contact( + 'BEGIN:VCARD\nUID:' + contact.uid + '\nEND:VCARD', + contact.addressbook + ) + localContact.updateContact(contact.jCal) + return localContact } } }, methods: { - - /** - * Fetch the selected contact from the store - * and store it as a local data for editing - */ - updateLocalContact() { - // create new local instance of this contact - let contact = this.$store.getters.getContact(this.uid) - this.contact = new Contact(ICAL.stringify(contact.jCal), contact.addressbook) - }, - /** * Executed on the 'updatedcontact' event * Send the local clone of contact to the store @@ -221,8 +259,31 @@ export default { }, toggleMenu() { this.openedMenu = !this.openedMenu + }, + + /** + * Move contact to the specified addressbook + * + * @param {Object} addressbook the desired addressbook + */ + moveContactToAddressbook(addressbook) { + addressbook = this.addressbooks.find( + search => search.id === addressbook.id + ) + // we need to use the store contact, not the local contact + let contact = this.$store.getters.getContact(this.contact.key) + // TODO Make sure we do not overwrite contacts + if (addressbook) { + this.$store + .dispatch('moveContactToAddressbook', { + contact: contact, + addressbook + }) + .then(() => { + this.updateContact() + }) + } } } - } </script> diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue index 7aa6b0c9..4260f447 100644 --- a/src/components/ContactDetails/ContactDetailsProperty.vue +++ b/src/components/ContactDetails/ContactDetailsProperty.vue @@ -24,7 +24,8 @@ <!-- If not in the rfcProps then we don't want to display it --> <component v-if="propModel && propType !== 'unknown'" :is="componentInstance" :select-type.sync="selectType" :prop-model="propModel" :value.sync="value" :is-first-property="isFirstProperty" - :class="{'property--last': isLastProperty}" :contact="contact" @delete="deleteProp" /> + :is-last-property="isLastProperty" :class="{'property--last': isLastProperty}" :contact="contact" + @delete="deleteProp" /> </template> <script> @@ -32,11 +33,11 @@ import { Property } from 'ical.js' import rfcProps from '../../models/rfcProps.js' import Contact from '../../models/contact' -import PropertyText from '../properties/PropertyText' -import PropertyMultipleText from '../properties/PropertyMultipleText' -import PropertyDateTime from '../properties/PropertyDateTime' -import propertyGroups from '../properties/PropertyGroups' -// import PropertySelect from '../properties/PropertyMultipleText' +import PropertyText from '../Properties/PropertyText' +import PropertyMultipleText from '../Properties/PropertyMultipleText' +import PropertyDateTime from '../Properties/PropertyDateTime' +import propertyGroups from '../Properties/PropertyGroups' +// import PropertySelect from '../Properties/PropertyMultipleText' export default { name: 'ContactDetailsProperty', @@ -98,7 +99,8 @@ export default { }, // is this the last property of its kind isLastProperty() { - if (this.index < this.sortedProperties.length) { + // array starts at 0, length starts at 1 + if (this.index < this.sortedProperties.length - 1) { return this.sortedProperties[this.index + 1].name !== this.propName } return true diff --git a/src/components/properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue index 2d6205b4..f4f843bc 100644 --- a/src/components/properties/PropertyDateTime.vue +++ b/src/components/Properties/PropertyDateTime.vue @@ -68,11 +68,13 @@ export default { }, propModel: { type: Object, - default: () => {} + default: () => {}, + required: true }, value: { type: VCardTime, - default: '' + default: '', + required: true }, isFirstProperty: { type: Boolean, @@ -100,6 +102,19 @@ export default { } }, + watch: { + /** + * Since we're updating a local data based on the value prop, + * we need to make sure to update the local data on pop change + */ + value: function() { + this.localValue = this.value + }, + selectType: function() { + this.localType = this.selectType + } + }, + methods: { /** diff --git a/src/components/properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue index 01976b4a..56a6ce8e 100644 --- a/src/components/properties/PropertyGroups.vue +++ b/src/components/Properties/PropertyGroups.vue @@ -53,15 +53,18 @@ export default { props: { propModel: { type: Object, - default: () => {} + default: () => {}, + required: true }, value: { type: Array, - default: () => [] + default: () => [], + required: true }, contact: { type: Contact, - default: null + default: null, + required: true } }, @@ -85,6 +88,19 @@ export default { } }, + watch: { + /** + * Since we're updating a local data based on the value prop, + * we need to make sure to update the local data on pop change + */ + value: function() { + this.localValue = this.value + }, + selectType: function() { + this.localType = this.selectType + } + }, + methods: { /** diff --git a/src/components/properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue index 1ac0e8ad..45fc70ef 100644 --- a/src/components/properties/PropertyMultipleText.vue +++ b/src/components/Properties/PropertyMultipleText.vue @@ -70,11 +70,13 @@ export default { }, propModel: { type: Object, - default: () => {} + default: () => {}, + required: true }, value: { type: [Array, Object], - default: () => [] + default: () => [], + required: true }, isFirstProperty: { type: Boolean, @@ -103,6 +105,19 @@ export default { } }, + watch: { + /** + * Since we're updating a local data based on the value prop, + * we need to make sure to update the local data on pop change + */ + value: function() { + this.localValue = this.value + }, + selectType: function() { + this.localType = this.selectType + } + }, + methods: { /** diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue new file mode 100644 index 00000000..4981eb47 --- /dev/null +++ b/src/components/Properties/PropertySelect.vue @@ -0,0 +1,147 @@ +<!-- + - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.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> + <div v-if="propModel" :class="`grid-span-${gridLength}`" class="property"> + <!-- title if first element --> + <property-title v-if="isFirstProperty && propModel.icon" :icon="propModel.icon" :readable-name="propModel.readableName" /> + + <div class="property__row"> + <!-- type selector --> + <multiselect v-if="propModel.options" v-model="localType" + :options="propModel.options" :searchable="false" :placeholder="t('contacts', 'Select type')" + class="multiselect-vue property__label" track-by="id" label="name" + @input="updateType" /> + + <!-- if we do not support any type on our model but one is set anyway --> + <div v-else-if="selectType" class="property__label">{{ selectType.name }}</div> + + <!-- no options, empty space --> + <div v-else class="property__label">{{ propModel.readableName }}</div> + + <!-- delete the prop --> + <button :title="t('contacts', 'Delete')" class="property__delete icon-delete" @click="deleteProperty" /> + + <multiselect v-model="localValue" :options="options" :placeholder="t('contacts', 'Select option')" + class="multiselect-vue property__value" track-by="id" label="name" + @input="updateValue" /> + </div> + </div> +</template> + +<script> +import Multiselect from 'vue-multiselect' +import propertyTitle from './PropertyTitle' +import debounce from 'debounce' + +export default { + name: 'PropertySelect', + + components: { + Multiselect, + propertyTitle + }, + + props: { + selectType: { + type: [Object, Boolean], + default: () => {} + }, + propModel: { + type: Object, + default: () => {}, + required: true + }, + value: { + type: [Object, String], + default: '', + required: true + }, + options: { + type: Array, + default: () => [], + required: true + }, + isFirstProperty: { + type: Boolean, + default: true + }, + isLastProperty: { + type: Boolean, + default: true + } + }, + + data() { + return { + localValue: this.value, + localType: this.selectType + } + }, + + computed: { + gridLength() { + let hasTitle = this.isFirstProperty && this.propModel.icon ? 1 : 0 + let isLast = this.isLastProperty ? 1 : 0 + // length is one & add one space at the end + return hasTitle + 1 + isLast + } + }, + + watch: { + /** + * Since we're updating a local data based on the value prop, + * we need to make sure to update the local data on pop change + */ + value: function() { + this.localValue = this.value + }, + selectType: function() { + this.localType = this.selectType + } + }, + + methods: { + + /** + * Delete the property + */ + deleteProperty() { + this.$emit('delete') + }, + + /** + * Debounce and send update event to parent + */ + updateValue: debounce(function(e) { + // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier + this.$emit('update:value', this.localValue) + }, 500), + + updateType: debounce(function(e) { + // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier + this.$emit('update:selectType', this.localType) + }, 500) + } +} + +</script> diff --git a/src/components/properties/PropertyText.vue b/src/components/Properties/PropertyText.vue index a15caa4d..32937db0 100644 --- a/src/components/properties/PropertyText.vue +++ b/src/components/Properties/PropertyText.vue @@ -67,11 +67,13 @@ export default { }, propModel: { type: Object, - default: () => {} + default: () => {}, + required: true }, value: { type: String, - default: '' + default: '', + required: true }, isFirstProperty: { type: Boolean, @@ -98,8 +100,21 @@ export default { return hasTitle + 1 + isLast } }, - methods: { + watch: { + /** + * Since we're updating a local data based on the value prop, + * we need to make sure to update the local data on pop change + */ + value: function() { + this.localValue = this.value + }, + selectType: function() { + this.localType = this.selectType + } + }, + + methods: { /** * Delete the property */ @@ -121,5 +136,4 @@ export default { }, 500) } } - </script> diff --git a/src/components/properties/PropertyTitle.vue b/src/components/Properties/PropertyTitle.vue index d857a647..cde06dd3 100644 --- a/src/components/properties/PropertyTitle.vue +++ b/src/components/Properties/PropertyTitle.vue @@ -34,11 +34,13 @@ export default { props: { icon: { type: String, - default: '' + default: '', + required: true }, readableName: { type: String, - default: '' + default: '', + required: true } } diff --git a/src/models/contact.js b/src/models/contact.js index b332e89d..e5e9e83e 100644 --- a/src/models/contact.js +++ b/src/models/contact.js @@ -48,6 +48,7 @@ export default class Contact { this.vCard.addPropertyWithValue('uid', uuid()) } } + /** * Update internal data of this contact * @@ -60,6 +61,16 @@ export default class Contact { } /** + * Update linked addressbook of this contact + * + * @param {Object} addressbook + * @memberof Contact + */ + updateAddressbook(addressbook) { + this.addressbook = addressbook + } + + /** * Ensure we're normalizing the possible arrays * into a string by taking the first element * e.g. ORG:ABC\, Inc.; will output an array because of the semi-colon diff --git a/src/models/rfcProps.js b/src/models/rfcProps.js index 55df4349..cae39970 100644 --- a/src/models/rfcProps.js +++ b/src/models/rfcProps.js @@ -54,7 +54,7 @@ const properties = { readableName: t('contacts', 'Federated Cloud ID'), defaultValue: { value: [''], - meta: { type: ['HOME'] } + type: ['HOME'] }, options: [ { id: 'HOME', name: t('contacts', 'Home') }, @@ -76,9 +76,10 @@ const properties = { ], displayOrder: [0, 2, 1, 5, 3, 4, 6], icon: 'icon-address', + default: true, defaultValue: { value: ['', '', '', '', '', '', ''], - meta: { type: ['HOME'] } + type: ['HOME'] }, options: [ { id: 'HOME', name: t('contacts', 'Home') }, @@ -105,9 +106,10 @@ const properties = { multiple: true, readableName: t('contacts', 'Email'), icon: 'icon-mail', + default: true, defaultValue: { value: '', - meta: { type: ['HOME'] } + type: ['HOME'] }, options: [ { id: 'HOME', name: t('contacts', 'Home') }, @@ -121,7 +123,7 @@ const properties = { icon: 'icon-comment', defaultValue: { value: [''], - meta: { type: ['SKYPE'] } + type: ['SKYPE'] }, options: [ { id: 'IRC', name: 'IRC' }, @@ -135,9 +137,10 @@ const properties = { multiple: true, readableName: t('contacts', 'Phone'), icon: 'icon-comment', + default: true, defaultValue: { value: '', - meta: { type: ['HOME,VOICE'] } + type: ['HOME,VOICE'] }, options: [ { id: 'HOME,VOICE', name: t('contacts', 'Home') }, @@ -161,7 +164,7 @@ const properties = { readableName: t('contacts', 'Social network'), defaultValue: { value: [''], - meta: { type: ['facebook'] } + type: ['facebook'] }, options: [ { id: 'FACEBOOK', name: 'Facebook' }, @@ -208,7 +211,7 @@ const properties = { ), defaultValue: { value: [''], - meta: { type: ['CONTACT'] } + type: ['CONTACT'] }, options: [ { id: 'CONTACT', name: t('contacts', 'Contact') }, diff --git a/src/services/cdav.js b/src/services/cdav.js new file mode 100644 index 00000000..baecc096 --- /dev/null +++ b/src/services/cdav.js @@ -0,0 +1,47 @@ +/** + * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.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/>. + * + */ + +import cdav from 'cdav-library' + +function xhrProvider() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'requesttoken': OC.requestToken + } + var xhr = new XMLHttpRequest() + var oldOpen = xhr.open + + // override open() method to add headers + xhr.open = function() { + var result = oldOpen.apply(this, arguments) + for (let name in headers) { + xhr.setRequestHeader(name, headers[name]) + } + return result + } + OC.registerXHRForErrorProcessing(xhr) + return xhr +} + +export default new cdav.DavClient({ + rootUrl: OC.linkToRemote('dav') +}, xhrProvider) diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js index 2dfe1aa8..775c4bf9 100644 --- a/src/store/addressbooks.js +++ b/src/store/addressbooks.js @@ -25,13 +25,16 @@ import vcfFile from '!raw-loader!./FakeName.vcf' import parseVcf from '../services/parseVcf' import Vue from 'vue' +import client from '../services/cdav' + const addressbookModel = { id: '', displayName: '', enabled: true, owner: '', shares: [], - contacts: {} + contacts: {}, + url: '' } const state = { @@ -80,8 +83,8 @@ const mutations = { * @param {Contact} contact */ addContactToAddressbook(state, contact) { - let addressbook = state.addressbooks.find(search => search === contact.addressbook) - Vue.set(addressbook.contacts, contact.key, contact) + let addressbook = state.addressbooks.find(search => search.id === contact.addressbook.id) + Vue.set(addressbook.contacts, contact.uid, contact) }, /** @@ -91,7 +94,7 @@ const mutations = { * @param {Contact} contact the contact to delete */ deleteContactFromAddressbook(state, contact) { - let addressbook = state.addressbooks.find(addressbook => addressbook === contact.addressbook) + let addressbook = state.addressbooks.find(search => search.id === contact.addressbook.id) Vue.delete(addressbook, contact.uid) }, @@ -163,50 +166,19 @@ const actions = { * @returns {Promise} fetch and commit */ async getAddressbooks(context) { - // Fake data before using real dav requests - let addressbooks = [ - { + let addressbooks = client.addressbookHomes.map(addressbook => { + return { id: 'ab1', displayName: 'Addressbook 1', enabled: true, owner: 'admin', - shares: [ - { displayname: 'Bob', writeable: true }, - { displayname: 'Rita', writeable: true }, - { displayname: 'Sue', writeable: false } - ], - contacts: {} - }, - { - id: 'ab2', - displayName: 'Addressbook 2', - enabled: false, - owner: 'admin', - shares: [ - { displayname: 'Aimee', writeable: false }, - { displayname: 'Jaguar', writeable: true } - ], - contacts: {} - }, - { - id: 'ab3', - displayName: 'Addressbook 3', - enabled: true, - owner: 'User1', - shares: [], - contacts: {} + dav: addressbook } - ] - // fake request - return new Promise((resolve, reject) => { - return setTimeout(() => { - addressbooks.forEach(addressbook => { - context.commit('addAddressbooks', addressbook) - }) - resolve() - return addressbooks - }, 1000) }) + addressbooks.forEach(addressbook => { + context.commit('addAddressbooks', addressbook) + }) + return addressbooks }, /** @@ -252,6 +224,20 @@ const actions = { */ shareAddressbook(contect, addressbook, sharee) { // Share addressbook with entered group or user + }, |