summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2018-09-25 18:32:30 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2018-09-25 18:32:30 +0200
commitd29267b00b5dc766e3d47448085650cb416fc10d (patch)
tree8fee9d75c5e918e5aab3c88a6567174aa3d79953 /src
parente7ae0b8fe235e1b793c4ccb2907f8d48f43eacbb (diff)
parent60e1a56251ea9df34858b65bdbddc9fcee67a568 (diff)
Merge branch 'vue' of https://github.com/nextcloud/contacts into vue-avatars-in-list
Diffstat (limited to 'src')
-rw-r--r--src/components/ContactDetails.vue171
-rw-r--r--src/components/ContactDetails/ContactDetailsProperty.vue17
-rw-r--r--src/components/ContentList/ContentListItem.vue2
-rw-r--r--src/components/Properties/PropertyDateTime.vue11
-rw-r--r--src/components/Properties/PropertyGroups.vue2
-rw-r--r--src/components/Properties/PropertyMultipleText.vue3
-rw-r--r--src/components/Properties/PropertySelect.vue53
-rw-r--r--src/components/Properties/PropertyText.vue3
-rw-r--r--src/components/Properties/PropertyTitle.vue7
-rw-r--r--src/components/Settings/SettingsAddressbook.vue56
-rw-r--r--src/components/Settings/SettingsAddressbookShare.vue11
-rw-r--r--src/components/Settings/SettingsNewAddressbook.vue45
-rw-r--r--src/models/contact.js36
-rw-r--r--src/models/rfcProps.js10
-rw-r--r--src/services/cdav.js44
-rw-r--r--src/store/FakeName.vcf1509
-rw-r--r--src/store/addressbooks.js218
-rw-r--r--src/store/contacts.js101
-rw-r--r--src/store/groups.js20
-rw-r--r--src/store/importState.js20
-rw-r--r--src/views/Contacts.vue86
21 files changed, 603 insertions, 1822 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index 41540e38..f72af6ff 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -71,15 +71,21 @@
<!-- actions -->
<div id="contact-header-actions">
- <div v-click-outside="closeMenu" class="menu-icon icon-more-white" @click="toggleMenu" />
- <div :class="{ 'open': openedMenu }" class="popovermenu">
- <popover-menu :menu="contactActions" />
+ <div v-tooltip.auto="warning" :class="{'icon-loading-small': loadingUpdate, 'icon-error-white menu-icon--pulse': warning}" class="menu-icon" />
+ <div class="menu-icon">
+ <div v-click-outside="closeMenu" class="icon-more-white" @click="toggleMenu" />
+ <div :class="{ 'open': openedMenu }" class="popovermenu">
+ <popover-menu :menu="contactActions" />
+ </div>
</div>
</div>
</header>
+ <!-- contact details loading -->
+ <section v-if="loadingData" class="icon-loading contact-details" />
+
<!-- contact details -->
- <section class="contact-details">
+ <section v-else class="contact-details">
<!-- properties iteration -->
<!-- using contact.key in the key and index as key to avoid conflicts between similar data and exact key -->
@@ -89,7 +95,7 @@
<!-- addressbook change select - no last property because class is not applied here-->
<property-select :prop-model="addressbookModel" :value.sync="addressbook" :is-first-property="true"
- :is-last-property="false" :options="addressbooksOptions" class="property--addressbooks" />
+ :is-last-property="false" class="property--addressbooks" />
<!-- new property select -->
<add-new-prop :contact="contact" />
@@ -143,17 +149,36 @@ export default {
data() {
return {
- openedMenu: false,
- addressbookModel: {
- readableName: t('contacts', 'Addressbook'),
- icon: 'icon-addressbook'
- }
+ /**
+ * Local off-store clone of the selected contact for edition
+ * because we can't edit contacts data outside the store.
+ * Every change will be dispatched and updated on the real
+ * store contact after a debounce.
+ */
+ localContact: undefined,
+ loadingData: true,
+ loadingUpdate: false,
+ openedMenu: false
}
},
computed: {
+
+ /**
+ * Warning message
+ *
+ * @returns {string}
+ */
+ warning() {
+ if (!this.contact.dav) {
+ return t('contacts', 'This contact is not yet synced. Edit it to trigger a change.')
+ }
+ },
+
/**
* Contact color based on uid
+ *
+ * @returns {string}
*/
colorAvatar() {
try {
@@ -166,6 +191,8 @@ export default {
/**
* Header actions for the contact
+ *
+ * @returns {Array}
*/
contactActions() {
let actions = [
@@ -188,6 +215,8 @@ export default {
/**
* Contact properties copied and sorted by rfcProps.fieldOrder
+ *
+ * @returns {Array}
*/
sortedProperties() {
return this.contact.properties.slice(0).sort((a, b) => {
@@ -197,20 +226,39 @@ export default {
})
},
- // usable addressbook object linked to the local contact
+ /**
+ * Fake model to use the propertySelect component
+ *
+ * @returns {Object}
+ */
+ addressbookModel() {
+ return {
+ readableName: t('contacts', 'Addressbook'),
+ icon: 'icon-addressbook',
+ options: this.addressbooksOptions
+ }
+ },
+
+ /**
+ * Usable addressbook object linked to the local contact
+ *
+ * @param {string} [addressbookId] set the addressbook id
+ * @returns {string}
+ */
addressbook: {
get: function() {
- return {
- id: this.contact.addressbook.id,
- name: this.contact.addressbook.displayName
- }
+ return this.contact.addressbook.id
},
- set: function(addressbook) {
- this.moveContactToAddressbook(addressbook)
+ set: function(addressbookId) {
+ this.moveContactToAddressbook(addressbookId)
}
},
- // store getters filtered and mapped to usable object
+ /**
+ * Store getters filtered and mapped to usable object
+ *
+ * @returns {Array}
+ */
addressbooksOptions() {
return this.addressbooks
.filter(addressbook => addressbook.readOnly)
@@ -226,29 +274,37 @@ export default {
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
+ return this.$store.getters.getContact(this.uid)
+ }
+ },
+
+ watch: {
+ contact: function() {
+ if (this.uid) {
+ this.selectContact(this.uid)
}
}
},
+ beforeMount() {
+ // load the desired data if we already selected a contact
+ if (this.uid) {
+ this.selectContact(this.uid)
+ }
+ },
+
methods: {
/**
* Executed on the 'updatedcontact' event
* Send the local clone of contact to the store
*/
updateContact() {
+ this.loadingUpdate = true
this.$store.dispatch('updateContact', this.contact)
+ .then(() => {
+ this.loadingUpdate = false
+ })
},
/**
@@ -268,28 +324,67 @@ export default {
},
/**
+ * Select a contac, and update the localContact
+ * Fetch updated data if necessary
+ *
+ * @param {string} uid the contact uid
+ */
+ selectContact(uid) {
+ // local version of the contact
+ this.loadingData = true
+ let contact = this.$store.getters.getContact(uid)
+
+ // if contact exists AND if exists on server
+ if (contact && contact.dav) {
+ this.$store.dispatch('fetchFullContact', contact)
+ .then(() => {
+ // create empty contact and copy inner data
+ let localContact = new Contact(
+ 'BEGIN:VCARD\nUID:' + contact.uid + '\nEND:VCARD',
+ contact.addressbook
+ )
+ localContact.updateContact(contact.jCal)
+ this.localContact = localContact
+ this.loadingData = false
+ })
+ .catch((error) => {
+ OC.Notification.showTemporary(t('contacts', 'The contact doesn\'t exists anymore on the server.'))
+ console.error(error)
+ // trigger a local deletion from the store only
+ this.$store.dispatch('deleteContact', { contact: this.contact, dav: false })
+ })
+ } else if (contact) {
+ // create empty contact and copy inner data
+ // wait for an update to really push the contact on the server!
+ this.localContact = new Contact(
+ 'BEGIN:VCARD\nUID:' + contact.uid + '\nEND:VCARD',
+ contact.addressbook
+ )
+ this.loadingData = false
+ }
+ },
+
+ /**
* Dispatch contact deletion request
*/
deleteContact() {
- this.$store.dispatch('deleteContact', this.contact)
+ this.$store.dispatch('deleteContact', { contact: this.contact })
},
/**
* Move contact to the specified addressbook
*
- * @param {Object} addressbook the desired addressbook
+ * @param {string} addressbookId the desired addressbook ID
*/
- 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)
+ moveContactToAddressbook(addressbookId) {
+ let addressbook = this.addressbooks.find(search => search.id === addressbookId)
// TODO Make sure we do not overwrite contacts
if (addressbook) {
this.$store
.dispatch('moveContactToAddressbook', {
- contact: contact,
+ // we need to use the store contact, not the local contact
+ // using this.contact and not this.localContact
+ contact: this.contact,
addressbook
})
.then(() => {
diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue
index 4b0428fb..289efe3a 100644
--- a/src/components/ContactDetails/ContactDetailsProperty.vue
+++ b/src/components/ContactDetails/ContactDetailsProperty.vue
@@ -22,10 +22,11 @@
<template>
<!-- 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"
- :property="property" :is-last-property="isLastProperty" :class="{'property--last': isLastProperty}"
- :contact="contact" @delete="deleteProp" />
+ <component v-if="propModel && propType !== 'unknown'"
+ :is="componentInstance" :select-type.sync="selectType" :prop-model="propModel"
+ :value.sync="value" :is-first-property="isFirstProperty" :property="property"
+ :is-last-property="isLastProperty" :class="{'property--last': isLastProperty}" :contact="contact"
+ @delete="deleteProp" />
</template>
<script>
@@ -37,7 +38,7 @@ 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 PropertySelect from '../Properties/PropertySelect'
export default {
name: 'ContactDetailsProperty',
@@ -76,6 +77,8 @@ export default {
return PropertyMultipleText
} else if (this.propType && ['date-and-or-time', 'date-time', 'time', 'date'].indexOf(this.propType) > -1) {
return PropertyDateTime
+ } else if (this.propType && this.propType === 'select') {
+ return PropertySelect
} else if (this.propType && this.propType !== 'unknown') {
return PropertyText
}
@@ -111,6 +114,10 @@ export default {
return this.property.name
},
propType() {
+ // if we have a force type set, use it!
+ if (this.propModel && this.propModel.force) {
+ return this.propModel.force
+ }
return this.property.getDefaultType()
},
diff --git a/src/components/ContentList/ContentListItem.vue b/src/components/ContentList/ContentListItem.vue
index 27036b01..2405bc4b 100644
--- a/src/components/ContentList/ContentListItem.vue
+++ b/src/components/ContentList/ContentListItem.vue
@@ -71,7 +71,7 @@ export default {
* Dispatch contact deletion request
*/
deleteContact() {
- this.$store.dispatch('deleteContact', this.contact)
+ this.$store.dispatch('deleteContact', { contact: this.contact })
},
/**
diff --git a/src/components/Properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue
index 264417da..db312bcd 100644
--- a/src/components/Properties/PropertyDateTime.vue
+++ b/src/components/Properties/PropertyDateTime.vue
@@ -23,7 +23,8 @@
<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" />
+ <property-title v-if="isFirstProperty && propModel.icon" :icon="propModel.icon" :readable-name="propModel.readableName"
+ :info="propModel.info" />
<div class="property__row">
<!-- type selector -->
@@ -63,6 +64,11 @@ import propertyTitle from './PropertyTitle'
* Using the Object as hared data since it's the only way
* for us to forcefully omit some data (no year, or no time... etc)
* and ths only common syntax between js Date, moment and VCardTime
+ *
+ * @param {Object} vcardTime ICAL.VCardTime data
+ * @param {string} type the input type e.g. date-time
+ * @param {string} locale the user locale
+ * @returns {string}
*/
let formatDateTime = function(vcardTime, type, locale) {
// this is the only possibility for us to ensure
@@ -111,6 +117,8 @@ let formatDateTime = function(vcardTime, type, locale) {
* inside a function declaration will represent the
* location of the call. So this = DatetimePicker.
* Therefore we can use any props we pass through datetime-picker
+ *
+ * @returns {string}
*/
DatetimePicker.methods.stringify = function() {
return formatDateTime(this.$parent.localValue, this.type, this.$parent.locale)
@@ -154,6 +162,7 @@ export default {
default: true
}
},
+
data() {
return {
localValue: this.value,
diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue
index be871175..2ab97b93 100644
--- a/src/components/Properties/PropertyGroups.vue
+++ b/src/components/Properties/PropertyGroups.vue
@@ -85,6 +85,8 @@ export default {
/**
* Format array of groups objects to a string for the popup
* Based on the ultiselect limit
+ *
+ * @returns {string} the additional groups
*/
formatGroupsTitle() {
return this.localValue.slice(3).join(', ')
diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue
index dcbf080f..6804c9e5 100644
--- a/src/components/Properties/PropertyMultipleText.vue
+++ b/src/components/Properties/PropertyMultipleText.vue
@@ -23,7 +23,8 @@
<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" />
+ <property-title v-if="isFirstProperty && propModel.icon" :icon="propModel.icon" :readable-name="propModel.readableName"
+ :info="propModel.info" />
<div class="property__row">
<!-- type selector -->
diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue
index fb1aa827..a6469f00 100644
--- a/src/components/Properties/PropertySelect.vue
+++ b/src/components/Properties/PropertySelect.vue
@@ -23,17 +23,13 @@
<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" />
+ <property-title v-if="isFirstProperty && propModel.icon" :icon="propModel.icon" :readable-name="propModel.readableName"
+ :info="propModel.info" />
<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>
+ <div v-if="selectType" class="property__label">{{ selectType.name }}</div>
<!-- no options, empty space -->
<div v-else class="property__label">{{ propModel.readableName }}</div>
@@ -41,7 +37,7 @@
<!-- 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')"
+ <multiselect v-model="matchedOptions" :options="propModel.options" :placeholder="t('contacts', 'Select option')"
class="multiselect-vue property__value" track-by="id" label="name"
@input="updateValue" />
</div>
@@ -76,11 +72,6 @@ export default {
default: '',
required: true
},
- options: {
- type: Array,
- default: () => [],
- required: true
- },
isFirstProperty: {
type: Boolean,
default: true
@@ -93,8 +84,9 @@ export default {
data() {
return {
- localValue: this.value,
- localType: this.selectType
+ // value is represented by the ID of the possible options
+ localValue: this.value
+ // localType: this.selectType
}
},
@@ -104,7 +96,22 @@ export default {
let isLast = this.isLastProperty ? 1 : 0
// length is one & add one space at the end
return hasTitle + 1 + isLast
+ },
+
+ // matching value to the options we provide
+ matchedOptions: {
+ get() {
+ let selected = this.propModel.options.find(option => option.id === this.localValue)
+ return selected || {
+ id: this.localValue,
+ name: this.localValue
+ }
+ },
+ set(value) {
+ this.localValue = value.id
+ }
}
+
},
watch: {
@@ -115,10 +122,10 @@ export default {
*/
value: function() {
this.localValue = this.value
- },
- selectType: function() {
- this.localType = this.selectType
}
+ // selectType: function() {
+ // this.localType = this.selectType
+ // }
},
methods: {
@@ -136,12 +143,12 @@ export default {
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)
+
+ // updateType: debounce(function(e) {
+ // // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
+ // this.$emit('update:selectType', this.localType)
+ // }, 500)
}
}
diff --git a/src/components/Properties/PropertyText.vue b/src/components/Properties/PropertyText.vue
index 9cf499c4..d5488bab 100644
--- a/src/components/Properties/PropertyText.vue
+++ b/src/components/Properties/PropertyText.vue
@@ -23,7 +23,8 @@
<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" />
+ <property-title v-if="isFirstProperty && propModel.icon" :icon="propModel.icon" :readable-name="propModel.readableName"
+ :info="propModel.info" />
<div class="property__row">
<!-- type selector -->
diff --git a/src/components/Properties/PropertyTitle.vue b/src/components/Properties/PropertyTitle.vue
index 360e813a..f60eaf36 100644
--- a/src/components/Properties/PropertyTitle.vue
+++ b/src/components/Properties/PropertyTitle.vue
@@ -24,6 +24,8 @@
<h3 class="property__title property__row">
<div :class="icon" class="property__label property__title--icon" />
{{ readableName }}
+ <!-- display tooltip with hint if available -->
+ <div v-tooltip.right="info" v-if="info" class="icon-details" />
</h3>
</template>
@@ -41,6 +43,11 @@ export default {
type: String,
default: '',
required: true
+ },
+ info: {
+ type: String,
+ default: '',
+ required: false
}
}
}
diff --git a/src/components/Settings/SettingsAddressbook.vue b/src/components/Settings/SettingsAddressbook.vue
index 32862b81..cb0cd07e 100644
--- a/src/components/Settings/SettingsAddressbook.vue
+++ b/src/components/Settings/SettingsAddressbook.vue
@@ -81,7 +81,6 @@ export default {
toggleEnabledLoading: false,
deleteAddressbookLoading: false,
renameLoading: false,
- downloadLoading: false,
copyLoading: false
}
},
@@ -104,9 +103,8 @@ export default {
},
{
href: this.addressbook.url + '?export',
- icon: this.downloadLoading ? 'icon-loading-small' : 'icon-download',
- text: t('contacts', 'Download'),
- action: this.downloadAddressbook
+ icon: 'icon-download',
+ text: t('contacts', 'Download')
}
]
@@ -171,20 +169,14 @@ export default {
this.$store.dispatch('toggleAddressbookEnabled', this.addressbook)
} catch (err) {
// error handling
+ console.error(err)
+ OC.Notification.showTemporary(t('contacts', 'Enabled toggle of addressbook was not successful.'))
} finally {
// stop loading status regardless of outcome
this.toggleEnabledLoading = false
}
}, 500)
},
- downloadAddressbook() {
- // change to loading status
- this.downloadLoading = true
- setTimeout(() => {
- // stop loading status regardless of outcome
- this.downloadLoading = false
- }, 1500)
- },
deleteAddressbook() {
// change to loading status
this.deleteAddressbookLoading = true
@@ -193,6 +185,8 @@ export default {
this.$store.dispatch('deleteAddressbook', this.addressbook)
} catch (err) {
// error handling
+ console.error(err)
+ OC.Notification.showTemporary(t('contacts', 'Delete addressbook was not successful.'))
} finally {
// stop loading status regardless of outcome
this.deleteAddressbookLoading = false
@@ -210,9 +204,11 @@ export default {
this.renameLoading = true
setTimeout(() => {
try {
- this.$store.dispatch('renameAddressbook', { addressbook, newName }) // .then(e.target.parent.classList.add())
+ this.$store.dispatch('renameAddressbook', { addressbook, newName })
} catch (err) {
// error handling
+ console.error(err)
+ OC.Notification.showTemporary(t('contacts', 'Renaming of addressbook was not successful.'))
} finally {
this.editingName = false
// stop loading status regardless of outcome
@@ -222,24 +218,28 @@ export default {
}
}, 500)
},
- copyLink() {
+ copyLink(event) {
// change to loading status
this.copyLoading = true
- // copy link for addressbook to clipboard
- this.$copyText(this.addressbook.url).then(e => {
- this.copySuccess = true
- this.copied = true
- }, e => {
- this.copySuccess = false
- this.copied = true
+ event.stopPropagation()
- })
- // timeout sets the text back to copy to show text was copied
- setTimeout(() => {
- // stop loading status regardless of outcome
- this.copyLoading = false
- this.copied = false
- }, 1500)
+ // copy link for addressbook to clipboard
+ this.$copyText(this.addressbook.url)
+ .then(e => {
+ event.preventDefault()
+ this.copySuccess = true
+ this.copied = true
+ // Notify addressbook was copied
+ OC.Notification.showTemporary(t('contacts', 'Addressbook copied to clipboard'))
+ }, e => {
+ this.copySuccess = false
+ this.copied = true
+ OC.Notification.showTemporary(t('contacts', 'Addressbook was not copied to clipboard.'))
+ }).then(() => {
+ // stop loading status regardless of outcome
+ this.copyLoading = false
+ this.copied = false
+ })
}
}
}
diff --git a/src/components/Settings/SettingsAddressbookShare.vue b/src/components/Settings/SettingsAddressbookShare.vue
index 5faec2bb..c195b6a0 100644
--- a/src/components/Settings/SettingsAddressbookShare.vue
+++ b/src/components/Settings/SettingsAddressbookShare.vue
@@ -105,7 +105,10 @@ export default {
/**
* Share addressbook
*
- * @param {Object} chosenUserOrGroup
+ * @param {Object} data destructuring object
+ * @param {string} data.sharee the sharee
+ * @param {string} data.id id
+ * @param {Boolean} data.group group