summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJessica <jessica@Absolventas-MacBook-Pro.local>2018-09-27 14:51:02 +0200
committerJessica <jessica@Absolventas-MacBook-Pro.local>2018-09-27 14:51:02 +0200
commit70e37ee8ffafa139ee470cc2f57b181ccec75cf7 (patch)
tree3b01c2e1b9eecfa1d81b341584024fa65cdeac53 /src
parent22401768b4427f88032bcce8cc5570bd44be06ef (diff)
parent309dbc5efd6d87eb89a1b6c99f7b957bcc3e4513 (diff)
Merge remote-tracking branch 'origin/vue' into vue-avatar-management
Diffstat (limited to 'src')
-rw-r--r--src/components/ContactDetails.vue183
-rw-r--r--src/components/ContactDetails/ContactDetailsProperty.vue19
-rw-r--r--src/components/ContentList/ContentListItem.vue13
-rw-r--r--src/components/Properties/PropertyDateTime.vue147
-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.vue223
-rw-r--r--src/components/Settings/SettingsAddressbookShare.vue11
-rw-r--r--src/components/Settings/SettingsImportContacts.vue5
-rw-r--r--src/components/Settings/SettingsNewAddressbook.vue60
-rw-r--r--src/components/SettingsSection.vue2
-rw-r--r--src/components/core/appNavigation.vue96
-rw-r--r--src/components/core/appNavigation/navigationItem.vue162
-rw-r--r--src/components/core/popoverMenu.vue51
-rw-r--r--src/components/core/popoverMenu/popoverItem.vue70
-rw-r--r--src/main.js10
-rw-r--r--src/models/contact.js36
-rw-r--r--src/models/rfcProps.js10
-rw-r--r--src/router/index.js2
-rw-r--r--src/services/cdav.js44
-rw-r--r--src/store/FakeName.vcf1507
-rw-r--r--src/store/addressbooks.js225
-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.vue107
29 files changed, 936 insertions, 2256 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index b466a6d5..530df74c 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -48,17 +48,17 @@
<!-- fullname, org, title -->
<div id="contact-header-infos">
<h2>
- <input id="contact-fullname" v-model="contact.fullName" :disabled="!contact.addressbook.enabled"
+ <input id="contact-fullname" v-model="contact.fullName" :disabled="!contact.addressbook.readOnly"
:placeholder="t('contacts', 'Name')" type="text" autocomplete="off"
autocorrect="off" spellcheck="false" name="fullname"
value="" @input="debounceUpdateContact">
</h2>
<div id="details-org-container">
- <input id="contact-org" v-model="contact.org" :disabled="!contact.addressbook.enabled"
+ <input id="contact-org" v-model="contact.org" :disabled="!contact.addressbook.readOnly"
:placeholder="t('contacts', 'Company')" type="text" autocomplete="off"
autocorrect="off" spellcheck="false" name="org"
value="" @input="debounceUpdateContact">
- <input id="contact-title" v-model="contact.title" :disabled="!contact.addressbook.enabled"
+ <input id="contact-title" v-model="contact.title" :disabled="!contact.addressbook.readOnly"
:placeholder="t('contacts', 'Title')" type="text" autocomplete="off"
autocorrect="off" spellcheck="false" name="title"
value="" @input="debounceUpdateContact">
@@ -67,15 +67,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 -->
@@ -85,7 +91,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" />
@@ -95,6 +101,7 @@
</template>
<script>
+import { PopoverMenu } from 'nextcloud-vue'
import ClickOutside from 'vue-click-outside'
import Vue from 'vue'
import VTooltip from 'v-tooltip'
@@ -103,7 +110,6 @@ import debounce from 'debounce'
import Contact from '../models/contact'
import rfcProps from '../models/rfcProps.js'
-import PopoverMenu from './core/popoverMenu'
import ContactProperty from './ContactDetails/ContactDetailsProperty'
import AddNewProp from './ContactDetails/ContactDetailsAddNewProp'
import PropertySelect from './Properties/PropertySelect'
@@ -141,17 +147,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 {
@@ -164,6 +189,8 @@ export default {
/**
* Header actions for the contact
+ *
+ * @returns {Array}
*/
contactActions() {
let actions = [
@@ -173,7 +200,7 @@ export default {
href: this.contact.url
}
]
- if (this.contact.addressbook.enabled) {
+ if (this.contact.addressbook.readOnly) {
actions.push({
icon: 'icon-delete',
text: t('contacts', 'Delete'),
@@ -186,6 +213,8 @@ export default {
/**
* Contact properties copied and sorted by rfcProps.fieldOrder
+ *
+ * @returns {Array}
*/
sortedProperties() {
return this.contact.properties.slice(0).sort((a, b) => {
@@ -195,23 +224,42 @@ 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.enabled)
+ .filter(addressbook => addressbook.readOnly)
.map(addressbook => {
return {
id: addressbook.id,
@@ -224,29 +272,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
+ })
},
/**
@@ -266,28 +322,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 dd352517..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,7 +114,11 @@ export default {
return this.property.name
},
propType() {
- return this.property.type
+ // if we have a force type set, use it!
+ if (this.propModel && this.propModel.force) {
+ return this.propModel.force
+ }
+ return this.property.getDefaultType()
},
// template to use
diff --git a/src/components/ContentList/ContentListItem.vue b/src/components/ContentList/ContentListItem.vue
index 6c2ccdbf..88e6bb78 100644
--- a/src/components/ContentList/ContentListItem.vue
+++ b/src/components/ContentList/ContentListItem.vue
@@ -7,10 +7,14 @@
class="app-content-list-item-checkbox checkbox" @keypress.enter.space.prevent.stop="toggleSelect">
<label :for="contact.key" @click.prevent.stop="toggleSelect" @keypress.enter.space.prevent.stop="toggleSelect" />
-->
- <div :style="{ 'backgroundColor': colorAvatar }" class="app-content-list-item-icon">{{ contact.displayName | firstLetter }}</div>
+ <div :style="{ 'backgroundColor': colorAvatar }" class="app-content-list-item-icon">
+ {{ contact.displayName | firstLetter }}
+ <!-- try to fetch the avatar only if the contact exists on the server -->
+ <div v-if="contact.dav" :style="{ 'backgroundImage': avatarUrl }" class="app-content-list-item-icon__avatar" />
+ </div>
<div class="app-content-list-item-line-one">{{ contact.displayName }}</div>
<div v-if="contact.email" class="app-content-list-item-line-two">{{ contact.email }}</div>
- <div v-if="contact.addressbook.enabled" class="icon-delete" tabindex="0"
+ <div v-if="contact.addressbook.readOnly" class="icon-delete" tabindex="0"
@click.prevent.stop="deleteContact" @keypress.enter.prevent.stop="deleteContact" />
</div>
</template>
@@ -48,6 +52,9 @@ export default {
} catch (e) {
return 'grey'
}
+ },
+ avatarUrl() {
+ return `url(${this.contact.url}?photo)`
}
},
methods: {
@@ -64,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 74f6ed72..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 -->
@@ -41,24 +42,95 @@
<!-- delete the prop -->
<button :title="t('contacts', 'Delete')" class="property__delete icon-delete" @click="deleteProperty" />
- <input v-model.trim="localValue" class="property__value" type="text"
- @input="updateValue">
+ <!-- Real input where the picker shows -->
+ <datetime-picker :value="localValue.toJSDate()" :minute-step="10" :lang="lang"
+ :clearable="false" :first-day-of-week="firstDay" :type="inputType"
+ confirm @confirm="updateValue" />
</div>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
-import propertyTitle from './PropertyTitle'
+import { DatetimePicker } from 'nextcloud-vue'
import debounce from 'debounce'
+import moment from 'moment'
import { VCardTime } from 'ical.js'
+import propertyTitle from './PropertyTitle'
+
+/**
+ * Format time with locale to display only
+ * 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
+ // no data is lost. e.g. if no second are set
+ // the second will be null and not 0
+ let datetimeData = vcardTime.toJSON()
+ let datetime = ''
+ /**
+ * Make sure to display the most interesting data.
+ * If the Object does not have any time, do not display
+ * the time and vice-versa.
+ */
+ // No hour, no minute and no second = date only
+ if (datetimeData.hour === null && datetimeData.minute === null && datetimeData.second === null) {
+ datetime = moment(datetimeData)
+ .locale(locale)
+ .format('LL')
+
+ // No year, no month and no day = time only
+ } else if (datetimeData.year === null && datetimeData.month === null && datetimeData.day === null) {
+ datetime = moment(datetimeData)
+ .locale(locale)
+ .format('LTS')
+ }
+
+ // Fallback to the data ical.js provide us
+ if (datetime === '') {
+ datetime = moment(datetimeData)
+ .locale(locale)
+ .format(
+ type === 'datetime'
+ ? 'LLLL' // date & time display
+ : type === 'date'
+ ? 'LL' // only date
+ : 'LTS' // only time
+ )
+ }
+ return datetimeData.year === null
+ // replace year and remove double spaces
+ ? datetime.replace(moment(vcardTime).year(), '').replace(/\s\s+/g, ' ')
+ : datetime
+}
+
+/**
+ * Override format function and use this since this
+ * 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)
+}
+
export default {
name: 'PropertyDateTime',
components: {
Multiselect,
- propertyTitle
+ propertyTitle,
+ DatetimePicker
},
props: {
@@ -76,6 +148,11 @@ export default {
default: '',
required: true
},
+ property: {
+ type: Object,
+ default: () => {},
+ required: true
+ },
isFirstProperty: {
type: Boolean,
default: true
@@ -89,7 +166,25 @@ export default {
data() {
return {
localValue: this.value,
- localType: this.selectType
+ localType: this.selectType,
+
+ // input type following DatePicker docs
+ inputType: this.property.getDefaultType() === 'date-time' || this.property.getDefaultType() === 'date-and-or-time'
+ ? 'datetime'
+ : this.property.getDefaultType() === 'date'
+ ? 'date'
+ : 'time',
+
+ // locale and lang data
+ locale: 'en', // temporary value, see mounted
+ firstDay: window.firstDay, // provided by nextcloud
+ lang: {
+ days: window.dayNamesShort, // provided by nextcloud
+ months: window.monthNamesShort, // provided by nextcloud
+ placeholder: {
+ date: t('contacts', 'Select Date')
+ }
+ }
}
},
@@ -116,6 +211,31 @@ export default {
}
},
+ mounted() {
+ // Load the locale
+ // convert format like en_GB to en-gb for `moment.js`
+ let locale = OC.getLocale().replace('_', '-').toLowerCase()
+
+ // default load e.g. fr-fr
+ import('moment/locale/' + this.locale)
+ .then(e => {
+ // force locale change to update
+ // the component once done loading
+ this.locale = locale
+ })
+ .catch(e => {
+ // failure: fallback to fr
+ import('moment/locale/' + locale.split('-')[0])
+ .then(e => {
+ this.locale = locale.split('-')[0]
+ })
+ .catch(e => {
+ // failure, fallback to english
+ this.locale = 'en'
+ })
+ })
+ },
+
methods: {
/**
@@ -129,7 +249,22 @@ export default {
* Debounce and send update event to parent
*/
updateValue: debounce(function(e) {
+ let rawData = moment(e).toArray()
+
+ /**
+ * Use the current year to ensure we do not lose
+ * the year data on v4.0 since we currently have
+ * no options to remove the year selection.
+ */
+ if (this.value.year === null) {
+ rawData[0] = null
+ }
+
+ // reset the VCardTime component to the selected date/time
+ this.localValue.resetTo(...rawData)
+
// https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
+ // Use moment to convert the JsDate to Object
this.$emit('update:value', this.localValue)
}, 500),
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="propMod