diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-09-25 18:32:30 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-09-25 18:32:30 +0200 |
commit | d29267b00b5dc766e3d47448085650cb416fc10d (patch) | |
tree | 8fee9d75c5e918e5aab3c88a6567174aa3d79953 /src | |
parent | e7ae0b8fe235e1b793c4ccb2907f8d48f43eacbb (diff) | |
parent | 60e1a56251ea9df34858b65bdbddc9fcee67a568 (diff) |
Merge branch 'vue' of https://github.com/nextcloud/contacts into vue-avatars-in-list
Diffstat (limited to 'src')
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 |