diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-11-10 10:06:01 +0100 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2018-11-12 21:38:01 +0100 |
commit | a79a8d6356b0e72627f085938b4bb34767525102 (patch) | |
tree | 2097c00bbd47cfa94f5611a49c29f10cf380011a /src | |
parent | 664d0685feb851604717e14edbd4458b20e5addc (diff) |
Sharing design and cdav request
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/ContactDetails.vue | 4 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsAddNewProp.vue | 4 | ||||
-rw-r--r-- | src/components/ContactDetails/ContactDetailsProperty.vue | 4 | ||||
-rw-r--r-- | src/components/Properties/PropertyTitle.vue | 2 | ||||
-rw-r--r-- | src/components/Settings/SettingsAddressbook.vue | 38 | ||||
-rw-r--r-- | src/components/Settings/SettingsAddressbookShare.vue | 105 | ||||
-rw-r--r-- | src/components/Settings/SettingsAddressbookSharee.vue | 76 | ||||
-rw-r--r-- | src/main.js | 4 | ||||
-rw-r--r-- | src/store/addressbooks.js | 131 | ||||
-rw-r--r-- | src/views/Contacts.vue | 4 |
10 files changed, 219 insertions, 153 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index 206aa749..35c3487b 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -109,7 +109,7 @@ <script> import debounce from 'debounce' -import rfcProps from '../models/rfcProps.js' +import rfcProps from 'Models/rfcProps' import ContactProperty from './ContactDetails/ContactDetailsProperty' import AddNewProp from './ContactDetails/ContactDetailsAddNewProp' @@ -408,9 +408,9 @@ export default { selectedContact: contact.key } }) - this.loadingUpdate = false } catch (error) { console.error(error) + } finally { this.loadingUpdate = false } } diff --git a/src/components/ContactDetails/ContactDetailsAddNewProp.vue b/src/components/ContactDetails/ContactDetailsAddNewProp.vue index c9d5404c..ddcdaf55 100644 --- a/src/components/ContactDetails/ContactDetailsAddNewProp.vue +++ b/src/components/ContactDetails/ContactDetailsAddNewProp.vue @@ -37,8 +37,8 @@ </template> <script> -import rfcProps from '../../models/rfcProps.js' -import Contact from '../../models/contact' +import rfcProps from 'Models/rfcProps' +import Contact from 'Models/contact' import PropertyTitle from 'Components/Properties/PropertyTitle' export default { diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue index e2eee5b6..dca5a7d9 100644 --- a/src/components/ContactDetails/ContactDetailsProperty.vue +++ b/src/components/ContactDetails/ContactDetailsProperty.vue @@ -32,8 +32,8 @@ <script> import { Property } from 'ical.js' -import rfcProps from '../../models/rfcProps.js' -import Contact from '../../models/contact' +import rfcProps from 'Models/rfcProps' +import Contact from 'Models/contact' import PropertyText from 'Components/Properties/PropertyText' import PropertyMultipleText from 'Components/Properties/PropertyMultipleText' diff --git a/src/components/Properties/PropertyTitle.vue b/src/components/Properties/PropertyTitle.vue index 18947907..8c6341d4 100644 --- a/src/components/Properties/PropertyTitle.vue +++ b/src/components/Properties/PropertyTitle.vue @@ -25,7 +25,7 @@ <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" /> + <div v-tooltip.auto="info" v-if="info" class="icon-details" /> </h3> </template> diff --git a/src/components/Settings/SettingsAddressbook.vue b/src/components/Settings/SettingsAddressbook.vue index 9a580cc9..ad51c967 100644 --- a/src/components/Settings/SettingsAddressbook.vue +++ b/src/components/Settings/SettingsAddressbook.vue @@ -25,8 +25,10 @@ <!-- addressbook name --> <span class="addressbook__name">{{ addressbook.displayName }}</span> <!-- sharing button --> - <a href="#" class="addressbook__share icon-shared" - @click="toggleShare" /> + <a v-tooltip.top="sharedWithTooltip" v-if="!addressbook.readOnly" + :class="{'addressbook__share--shared': hasShares}" + :title="sharedWithTooltip" href="#" + class="addressbook__share icon-shared" @click="toggleShare" /> <!-- popovermenu --> <a v-click-outside="closeMenu" href="#" class="addressbook__menu"> <div class="icon-more" @click="toggleMenu" /> @@ -35,7 +37,7 @@ </div> </a> <!-- sharing input --> - <share-address-book v-if="shareOpen" :addressbook="addressbook" /> + <share-address-book v-if="shareOpen && !addressbook.readOnly" :addressbook="addressbook" /> </li> </template> @@ -74,6 +76,20 @@ export default { enabled() { return this.addressbook.enabled }, + hasShares() { + return this.addressbook.shares.length > 0 + }, + // info tooltip about number of shares + sharedWithTooltip() { + return this.hasShares + ? n('contacts', + 'Shared with {num} entity', + 'Shared with {num} entities', + this.addressbook.shares.length, { + num: this.addressbook.shares.length + }) + : '' // disable the tooltip + }, // building the popover menu menu() { let menu = [ @@ -114,14 +130,14 @@ export default { action: this.toggleAddressbookEnabled }) - } - // check to ensure last addressbook is not deleted. - if (this.$store.getters.getAddressbooks.length > 1) { - menu.push({ - icon: this.deleteAddressbookLoading ? 'icon-loading-small' : 'icon-delete', - text: t('contacts', 'Delete'), - action: this.deleteAddressbook - }) + // check to ensure last addressbook is not deleted. + if (this.$store.getters.getAddressbooks.length > 1) { + menu.push({ + icon: this.deleteAddressbookLoading ? 'icon-loading-small' : 'icon-delete', + text: t('contacts', 'Delete'), + action: this.deleteAddressbook + }) + } } return menu } diff --git a/src/components/Settings/SettingsAddressbookShare.vue b/src/components/Settings/SettingsAddressbookShare.vue index 0c87d799..5f0807fc 100644 --- a/src/components/Settings/SettingsAddressbookShare.vue +++ b/src/components/Settings/SettingsAddressbookShare.vue @@ -27,37 +27,26 @@ :options="usersOrGroups" :searchable="true" :internal-search="false" - :options-limit="250" - :limit="3" :max-height="600" :show-no-results="true" :placeholder="placeholder" :class="{ 'showContent': inputGiven, 'icon-loading': isLoading }" + :user-select="true" open-direction="bottom" - @search-change="asyncFind" - @input="shareAddressbook"> - <template slot="singleLabel" slot-scope="props"> - <span class="option__desc"> - <span class="option__title">{{ props.option.matchpattern }}</span> - </span> - </template> - <template slot="option" slot-scope="props"> - <div class="option__desc"> - <span>{{ props.option.matchstart }}</span><span class="addressbook-shares__shareematch--bold">{{ props.option.matchpattern }}</span><span>{{ props.option.matchend }} {{ props.option.matchtag }}</span> - </div> - </template> - <span slot="noResult">{{ noResult }} </span> - </multiselect> + track-by="user" + label="user" + @search-change="findSharee" + @input="shareAddressbook" /> <!-- list of user or groups addressbook is shared with --> <ul v-if="addressbook.shares.length > 0" class="addressbook-shares__list"> - <address-book-sharee v-for="sharee in addressbook.shares" :key="sharee.displayname + sharee.group" :sharee="sharee" /> + <address-book-sharee v-for="sharee in addressbook.shares" :key="sharee.uri" + :sharee="sharee" :addressbook="addressbook" /> </ul> </div> </template> <script> - -import api from 'Services/api' +import client from 'Services/cdav' import addressBookSharee from './SettingsAddressbookSharee' import debounce from 'debounce' @@ -99,44 +88,14 @@ export default { * Share addressbook * * @param {Object} data destructuring object - * @param {string} data.sharee the sharee - * @param {string} data.id id - * @param {Boolean} data.group group + * @param {string} data.user the userId + * @param {string} data.displayName the displayName + * @param {string} data.uri the sharing principalScheme uri + * @param {Boolean} data.isGroup is this a group ? */ - shareAddressbook({ sharee, id, group }) { + shareAddressbook({ user, displayName, uri, isGroup }) { let addressbook = this.addressbook - this.$store.dispatch('shareAddressbook', { addressbook, sharee, id, group }) - }, - /** - * Format responses from axios.all and add them to the option array - * - * @param {Array} matches array of matches returned from the axios request - * @param {String} query the search query - * @param {Boolean} group Is this a group? - */ - formatMatchResults(matches, query, group) { - if (matches.length < 1) { - return - } - let regex = new RegExp(query, 'i') - let existingSharees = this.addressbook.shares.map(share => share.id + share.group) - matches = matches.filter(share => existingSharees.indexOf(share.id + group) === -1) - // this.usersOrGroups.concat( - this.usersOrGroups = this.usersOrGroups.concat(matches.map(match => { - let matchResult = match.displayname.split(regex) - if (matchResult.length < 1) { - return - } - return { - sharee: match.displayname, - id: match.id, - matchstart: matchResult[0], - matchpattern: match.displayname.match(regex)[0], - matchend: matchResult[1], - matchtag: group ? '(group)' : '(user)', - group - } - })) + this.$store.dispatch('shareAddressbook', { addressbook, user, displayName, uri, isGroup }) }, /** @@ -144,30 +103,26 @@ export default { * * @param {String} query */ - asyncFind: debounce(function(query) { + findSharee: debounce(async function(query) { this.isLoading = true this.usersOrGroups = [] if (query.length > 0) { - api.all([ - api.get(OC.linkToOCS('cloud', 2) + 'users/details?search=' + query), - api.get(OC.linkToOCS('cloud', 2) + 'groups/details?search=' + query) - ]).then(response => { - let matchingUsers = Object.values(response[0].data.ocs.data.users) - let matchingGroups = response[1].data.ocs.data.groups - try { - this.formatMatchResults(matchingUsers, query, false) - } catch (error) { - console.debug(error) + const results = await client.principalPropertySearchByDisplayname(query) + this.usersOrGroups = results.reduce((list, result) => { + if (['GROUP', 'INDIVIDUAL'].indexOf(result.calendarUserType) > -1) { + const isGroup = result.calendarUserType === 'GROUP' + list.push({ + user: result[isGroup ? 'groupId' : 'userId'], + displayName: result.displayname, + icon: isGroup ? 'icon-group' : 'icon-user', + uri: result.principalScheme, + isGroup + }) } - try { - this.formatMatchResults(matchingGroups, query, true) - } catch (error) { - console.debug(error) - } - }).then(() => { - this.isLoading = false - this.inputGiven = true - }) + return list + }, []) + this.isLoading = false + this.inputGiven = true } else { this.inputGiven = false this.isLoading = false diff --git a/src/components/Settings/SettingsAddressbookSharee.vue b/src/components/Settings/SettingsAddressbookSharee.vue index 613bd301..3ea0722d 100644 --- a/src/components/Settings/SettingsAddressbookSharee.vue +++ b/src/components/Settings/SettingsAddressbookSharee.vue @@ -22,17 +22,26 @@ <template> <li class="addressbook-sharee"> - <span :class="sharee.group ? 'icon-group' : 'icon-user'" class="icon" /> - <span class="addressbook-sharee__identifier">{{ sharee.displayname }}</span> + <span :class="{ + 'icon-loading-small': loading, + 'icon-group': sharee.isGroup && !loading, + 'icon-user': !sharee.isGroup && !loading + }" class="icon" /> + <span class="addressbook-sharee__identifier">{{ sharee.displayName }}</span> <span class="addressbook-sharee__utils"> <input - :id="sharee.displayname" - v-model="writeable" + :id="uid" + :checked="writeable" + :disabled="loading" class="checkbox" name="editable" - type="checkbox"> - <label :for="sharee.displayName" @click="editSharee"> can edit</label> - <a href="#" title="Delete" + type="checkbox" + @change="editSharee"> + <label :for="uid"> + {{ t('contacts', 'can edit') }} + </label> + <a :class="{'addressbook-sharee__utils--disabled': loading}" href="#" + title="Delete" class="icon-delete" @click="deleteSharee" /> </span> @@ -45,24 +54,69 @@ export default { name: 'SettingsShareSharee', props: { + addressbook: { + type: Object, + required: true + }, sharee: { type: Object, required: true } }, + data() { + return { + loading: false + } + }, + computed: { writeable() { return this.sharee.writeable + }, + // generated id for this sharee + uid() { + return this.sharee.id + this.addressbook.id + Math.floor(Math.random() * 1000) } }, methods: { - deleteSharee() { - setTimeout(() => { this.$store.dispatch('removeSharee', this.sharee) }, 500) + async deleteSharee() { + if (this.loading) { + return false + } + + this.loading = true + try { + await this.$store.dispatch('removeSharee', { + addressbook: this.addressbook, + uri: this.sharee.uri + }) + } catch (error) { + console.error(error) + OC.Notification.showTemporary(t('contacts', 'Unable to delete the share.')) + } finally { + this.loading = false + } }, - editSharee() { - this.$store.dispatch('toggleShareeWritable', this.sharee) + async editSharee() { + if (this.loading) { + return false + } + + this.loading = true + try { + await this.$store.dispatch('toggleShareeWritable', { + addressbook: this.addressbook, + uri: this.sharee.uri, + writeable: !this.sharee.writeable + }) + } catch (error) { + console.error(error) + OC.Notification.showTemporary(t('contacts', 'Unable to change permissions.')) + } finally { + this.loading = false + } } } } diff --git a/src/main.js b/src/main.js index dd27bd7d..797f32dd 100644 --- a/src/main.js +++ b/src/main.js @@ -30,7 +30,7 @@ import { sync } from 'vuex-router-sync' /** GLOBAL COMPONENTS AND DIRECTIVE */ import { AppNavigation, DatetimePicker, Multiselect, PopoverMenu } from 'nextcloud-vue' import ClickOutside from 'vue-click-outside' -import Tooltip from 'v-tooltip' +import { VTooltip } from 'v-tooltip' import VueClipboard from 'vue-clipboard2' // CSP config for webpack dynamic chunk loading @@ -49,7 +49,7 @@ Vue.component('DatetimePicker', DatetimePicker) Vue.component('Multiselect', Multiselect) Vue.component('PopoverMenu', PopoverMenu) Vue.directive('ClickOutside', ClickOutside) -Vue.directive('Tooltip', Tooltip) +Vue.directive('Tooltip', VTooltip) Vue.use(VueClipboard) sync(store, router) diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js index 7f28f5fb..86720f1b 100644 --- a/src/store/addressbooks.js +++ b/src/store/addressbooks.js @@ -23,10 +23,12 @@ import Vue from 'vue' import ICAL from 'ical.js' +import pLimit from 'p-limit' + +import Contact from 'Models/contact' + import parseVcf from 'Services/parseVcf' import client from 'Services/cdav' -import Contact from 'Models/contact' -import pLimit from 'p-limit' const addressbookModel = { id: '', @@ -59,7 +61,28 @@ export function mapDavCollectionToAddressbook(addressbook) { owner: addressbook.owner, readOnly: addressbook.readOnly === true, url: addressbook.url, - dav: addressbook + dav: addressbook, + shares: addressbook.shares.map(sharee => Object.assign({}, mapDavShareeToSharee(sharee))) + } +} + +/** + * map a dav collection to our addressbook object model + * + * @param {Object} sharee the sharee object from the cdav library shares + * @returns {Object} + */ +export function mapDavShareeToSharee(sharee) { + const id = sharee.href.split('/').slice(-1)[0] + const name = sharee['common-name'] + ? sharee['common-name'] + : id + return { + displayName: name, + id: id, + writeable: sharee.access[0].endsWith('read-write'), + isGroup: sharee.href.startsWith('principal:principals/groups/'), + uri: sharee.href } } @@ -73,11 +96,11 @@ const mutations = { */ addAddressbook(state, addressbook) { // extend the addressbook to the default model - addressbook = Object.assign({}, addressbookModel, addressbook) + const newAddressbook = Object.assign({}, addressbookModel, addressbook) // force reinit of the contacts object to prevent // data passed as references - addressbook.contacts = {} - state.addressbooks.push(addressbook) + newAddressbook.contacts = {} + state.addressbooks.push(newAddressbook) }, /** @@ -162,17 +185,19 @@ const mutations = { * @param {Object} state the store data * @param {Object} data destructuring object * @param {Object} data.addressbook the addressbook - * @param {string} data.sharee the sharee - * @param {string} data.id id - * @param {Boolean} data.group group + * @param {string} data.user the userId + * @param {string} data.displayName the displayName + * @param {string} data.uri the sharing principalScheme uri + * @param {Boolean} data.isGroup is this a group ? */ - shareAddressbook(state, { addressbook, sharee, id, group }) { + shareAddressbook(state, { addressbook, user, displayName, uri, isGroup }) { addressbook = state.addressbooks.find(search => search.id === addressbook.id) - let newSharee = { - displayname: sharee, - id, + const newSharee = { + displayName, + id: user, writeable: false, - group + isGroup, + uri } addressbook.shares.push(newSharee) }, @@ -181,34 +206,27 @@ const mutations = { * Remove Sharee from addressbook shares list * * @param {Object} state the store data - * @param {Object} sharee the sharee + * @param {Object} data destructuring object + * @param {Object} data.addressbook the addressbook + * @param {string} data.uri the sharee uri */ - removeSharee(state, sharee) { - let addressbook = state.addressbooks.find(search => { - for (let i in search.shares) { - if (search.shares[i] === sharee) { - return true - } - } - }) - addressbook.shares.splice(addressbook.shares.indexOf(sharee), 1) + removeSharee(state, { addressbook, uri }) { + addressbook = state.addressbooks.find(search => search.id === addressbook.id) + let shareIndex = addressbook.shares.findIndex(sharee => sharee.uri === uri) + addressbook.shares.splice(shareIndex, 1) }, /** * Toggle sharee's writable permission * * @param {Object} state the store data - * @param {Object} sharee the sharee + * @param {Object} data destructuring object + * @param {Object} data.addressbook the addressbook + * @param {string} data.uri the sharee uri */ - updateShareeWritable(state, sharee) { - let addressbook = state.addressbooks.find(search => { - for (let i in search.shares) { - if (search.shares[i] === sharee) { - return true - } - } - }) - sharee = addressbook.shares.find(search => search === sharee) + updateShareeWritable(state, { addressbook, uri }) { + addressbook = state.addressbooks.find(search => search.id === addressbook.id) + let sharee = addressbook.shares.find(sharee => sharee.uri === uri) sharee.writeable = !sharee.writeable } @@ -230,6 +248,7 @@ const actions = { let addressbooks = await client.addressBookHomes[0].findAllAddressBooks() .then(addressbooks => { return addressbooks.map(addressbook => { + // formatting addressbooks return mapDavCollectionToAddressbook(addressbook) }) }) @@ -390,32 +409,54 @@ const actions = { /** * Remove sharee from Addressbook * @param {Object} context the store mutations Current context - * @param {Object} sharee Addressbook sharee object + * @param {Object} data destructuring object + * @param {Object} data.addressbook the addressbook + * @param {string} data.uri the sharee uri */ - removeSharee(context, sharee) { - context.commit('removeSharee', sharee) + async removeSharee(context, { addressbook, uri }) { + try { + await addressbook.dav.unshare(uri) + context.commit('removeSharee', { addressbook, uri }) + } catch (error) { + throw error + } }, /** * Toggle permissions of Addressbook Sharees writeable rights * @param {Object} context the store mutations Current context - * @param {Object} sharee Addressbook sharee object + * @param {Object} data destructuring object + * @param {Object} data.addressbook the addressbook + * @param {string} data.uri the sharee uri + * @param {Boolean} data.writeable the sharee permission */ - toggleShareeWritable(context, sharee) { - context.commit('updateShareeWritable', sharee) + async toggleShareeWritable(context, { addressbook, uri, writeable }) { + try { + await addressbook.dav.share(uri, writeable) + context.commit('updateShareeWritable', { addressbook, uri, writeable }) + } catch (error) { + throw error + } + }, /** * Share Adressbook with User or Group * @param {Object} context the store mutations Current context * @param {Object} data.addressbook the addressbook - * @param {String} data.sharee the sharee - * @param {Boolean} data.id id - * @param {Boolean} data.group group + * @param {string} data.user the userId + * @param {string} data.displayName the displayName + * @param {string} data.uri the sharing principalScheme uri + * @param {Boolean} data.isGroup is this a group ? */ - shareAddressbook(context, { addressbook, sharee, id, group }) { + async shareAddressbook(context, { addressbook, user, displayName, uri, isGroup }) { // Share addressbook with entered group or user - context.commit('shareAddressbook', { addressbook, sharee, id, group }) + try { + await addressbook.dav.share(uri) + context.commit('shareAddressbook', { addressbook, user, displayName, uri, isGroup }) + } catch (error) { + throw error + } }, /** diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue index f751470a..447df612 100644 --- a/src/views/Contacts.vue +++ b/src/views/Contacts.vue @@ -54,9 +54,9 @@ import ContactDetails from 'Components/ContactDetails' import ImportScreen from 'Components/ImportScreen' import Contact from 'Models/contact' -import rfcProps from 'Models/rfcProps.js' +import rfcProps from 'Models/rfcProps' -import client from 'Services/cdav.js' +import client from 'Services/cdav' export default { name: 'Contacts', |