summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2018-11-10 10:06:01 +0100
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2018-11-12 21:38:01 +0100
commita79a8d6356b0e72627f085938b4bb34767525102 (patch)
tree2097c00bbd47cfa94f5611a49c29f10cf380011a /src
parent664d0685feb851604717e14edbd4458b20e5addc (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.vue4
-rw-r--r--src/components/ContactDetails/ContactDetailsAddNewProp.vue4
-rw-r--r--src/components/ContactDetails/ContactDetailsProperty.vue4
-rw-r--r--src/components/Properties/PropertyTitle.vue2
-rw-r--r--src/components/Settings/SettingsAddressbook.vue38
-rw-r--r--src/components/Settings/SettingsAddressbookShare.vue105
-rw-r--r--src/components/Settings/SettingsAddressbookSharee.vue76
-rw-r--r--src/main.js4
-rw-r--r--src/store/addressbooks.js131
-rw-r--r--src/views/Contacts.vue4
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',