summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJessica Greene <jessica0greene@gmail.com>2018-08-28 16:05:37 +0200
committerGitHub <noreply@github.com>2018-08-28 16:05:37 +0200
commitd0517b8a42662f7323fa54f09b1eb85377be90d2 (patch)
tree96557d051ea31f1a71f492976385532335243a81 /src
parent670493bd8a029d8e3b6ab75778cb2fd0cb16378a (diff)
parentea5973c1a58c2dcca8bafd94e6233aac96155e18 (diff)
Merge pull request #595 from nextcloud/vue-share-addressbook
Vue share addressbook 🌮
Diffstat (limited to 'src')
-rw-r--r--src/components/Settings/SettingsAddressbook.vue2
-rw-r--r--src/components/Settings/SettingsAddressbookShare.vue172
-rw-r--r--src/components/Settings/SettingsAddressbookSharee.vue3
-rw-r--r--src/components/Settings/SettingsNewAddressbook.vue9
-rw-r--r--src/components/Settings/SettingsSortContacts.vue4
-rw-r--r--src/services/api.js127
-rw-r--r--src/store/addressbooks.js38
7 files changed, 277 insertions, 78 deletions
diff --git a/src/components/Settings/SettingsAddressbook.vue b/src/components/Settings/SettingsAddressbook.vue
index a143c8ba..fee2e324 100644
--- a/src/components/Settings/SettingsAddressbook.vue
+++ b/src/components/Settings/SettingsAddressbook.vue
@@ -50,7 +50,7 @@ import shareAddressBook from './SettingsAddressbookShare'
import clickOutside from 'vue-click-outside'
export default {
- name: 'SettingsAddressBook',
+ name: 'SettingsAddressbook',
components: {
popoverMenu,
shareAddressBook,
diff --git a/src/components/Settings/SettingsAddressbookShare.vue b/src/components/Settings/SettingsAddressbookShare.vue
index 0f6bca42..f64c5df5 100644
--- a/src/components/Settings/SettingsAddressbookShare.vue
+++ b/src/components/Settings/SettingsAddressbookShare.vue
@@ -21,58 +21,59 @@
-->
<template>
- <div class="addressbook__shares">
- <div class="dropdown-menu">
- <label class="typo__label" for="ajax">Async multiselect</label>
- <multiselect
- id="ajax"
- v-model="selectedUserOrGroup"
- :options="usersOrGroups"
- :multiple="true"
- :searchable="true"
- :loading="isLoading"
- :internal-search="false"
- :clear-on-select="false"
- :close-on-select="false"
- :options-limit="250"
- :limit="3"
- :limit-text="limitText"
- :max-height="600"
- :show-no-results="false"
- :hide-selected="true"
- label="name"
- track-by="code"
- placeholder="Type to search"
- open-direction="bottom"
- @search-change="asyncFind">
- <template slot="clear" slot-scope="props">
- <div v-if="selectedUserOrGroup.length" class="multiselect__clear" @mousedown.prevent.stop="clearAll(props.search)" />
- </template>
- <span slot="noResult">Oops! No elements found. Consider changing the search query.</span>
- </multiselect>
- <pre class="language-json"><code>{{ selectedUserOrGroup }}</code></pre>
- </div>
+ <div class="addressbook-shares">
+ <multiselect
+ id="users-groups-search"
+ :options="usersOrGroups"
+ :searchable="true"
+ :loading="isLoading"
+ :internal-search="false"
+ :options-limit="250"
+ :limit="3"
+ :max-height="600"
+ :show-no-results="true"
+ :placeholder="placeholder"
+ :class="{ 'showContent': inputGiven }"
+ open-direction="bottom"
+ class="multiselect-vue"
+ @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>
<!-- 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.name" :sharee="sharee" />
+ <address-book-sharee v-for="sharee in addressbook.shares" :key="sharee.displayname + sharee.group" :sharee="sharee" />
</ul>
</div>
</template>
<script>
import clickOutside from 'vue-click-outside'
+import api from '../../services/api'
import Multiselect from 'vue-multiselect'
import addressBookSharee from './SettingsAddressbookSharee'
+import debounce from 'debounce'
export default {
- name: 'SettingsShareAddressBook',
+ name: 'SettingsShareAddressbook',
components: {
clickOutside,
Multiselect,
addressBookSharee
},
directives: {
- clickOutside
+ clickOutside,
+ debounce
},
props: {
addressbook: {
@@ -85,33 +86,96 @@ export default {
data() {
return {
isLoading: false,
- usersOrGroups: [],
- selectedUserOrGroup: []
+ inputGiven: false,
+ usersOrGroups: []
+ }
+ },
+ computed: {
+ placeholder() {
+ return t('contacts', 'Share with users or groups')
+ },
+ noResult() {
+ return t('contacts', 'No users or groups')
}
},
methods: {
- limitText(count) {
- return `and ${count} other users or groups`
+ /**
+ * Share addressbook
+ *
+ * @param {Object} chosenUserOrGroup
+ */
+ shareAddressbook({ sharee, id, group }) {
+ let addressbook = this.addressbook
+ this.$store.dispatch('shareAddressbook', { addressbook, sharee, id, group })
},
- /* example :OC.linkToOCS('cloud', 2)+ 'groups?search=Test' */
- asyncFind(query) {
- this.isLoading = true
- this.usersOrGroups = []
- // let response = OC.linkToOCS('cloud', 2) + 'groups?search=' + query
- fetch(OC.linkToOCS('cloud', 2) + 'groups?search=' + query).then(response => {
- this.usersOrGroups.push(response)
- this.isLoading = false
- })
+ /**
+ * 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
+ * @param {Boolean} 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
+ }
+ }))
console.log(this.usersOrGroups) // eslint-disable-line
- /* ajaxFindCountry(query).then(response => {
- this.countries = response
- this.isLoading = false
- }) */
},
- clearAll() {
- this.selectedUserOrGroup = []
- }
+
+ /**
+ * Use Axios api call to find matches to the query from the existing Users & Groups
+ *
+ * @param {String} query
+ */
+ asyncFind: debounce(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)
+ }
+ try {
+ this.formatMatchResults(matchingGroups, query, true)
+ } catch (error) {
+ console.debug(error)
+ }
+ }).then(() => {
+
+ this.isLoading = false
+ })
+ this.inputGiven = true
+ } else {
+ this.inputGiven = false
+ }
+ }, 500)
}
}
</script>
diff --git a/src/components/Settings/SettingsAddressbookSharee.vue b/src/components/Settings/SettingsAddressbookSharee.vue
index f28b35ed..dcb00d03 100644
--- a/src/components/Settings/SettingsAddressbookSharee.vue
+++ b/src/components/Settings/SettingsAddressbookSharee.vue
@@ -22,7 +22,7 @@
<template>
<li class="addressbook__sharee">
- <span class="icon icon-group" />
+ <span :class="sharee.group ? 'icon-group' : 'icon-user'" class="icon" />
<span class="addressbook__sharee__identifier">{{ sharee.displayname }}</span>
<span class="addressbook__sharee__utils">
<input
@@ -66,7 +66,6 @@ export default {
setTimeout(() => { this.$store.dispatch('removeSharee', this.sharee) }, 500)
},
editSharee() {
- // not working yet need to work on!
this.$store.dispatch('toggleShareeWritable', this.sharee)
}
}
diff --git a/src/components/Settings/SettingsNewAddressbook.vue b/src/components/Settings/SettingsNewAddressbook.vue
index f403c455..d31fee0e 100644
--- a/src/components/Settings/SettingsNewAddressbook.vue
+++ b/src/components/Settings/SettingsNewAddressbook.vue
@@ -40,7 +40,7 @@
import clickOutside from 'vue-click-outside'
export default {
- name: 'SettingsNewAddressBook',
+ name: 'SettingsNewAddressbook',
components: {
clickOutside
},
@@ -72,9 +72,10 @@ export default {
* @returns {Promise}
*/
addAddressBook() {
- // let addressBook = this.$refs.addressBook.value
- // let addressBooks = this.$store.getters.getAddressbooks
- // let newAddressBooksArray = addressBooks.push(addressBook)
+ let addressBook = this.$refs.addressBook.value
+ let addressBooks = this.$store.getters.getAddressbooks
+ let newAddressBooksArray = addressBooks.push(addressBook)
+ return newAddressBooksArray
}
}
}
diff --git a/src/components/Settings/SettingsSortContacts.vue b/src/components/Settings/SettingsSortContacts.vue
index 440e7d00..72e2b6c7 100644
--- a/src/components/Settings/SettingsSortContacts.vue
+++ b/src/components/Settings/SettingsSortContacts.vue
@@ -66,11 +66,11 @@ export default {
key: 'firstName'
},
{
- label: t('contacts', 'Lastname'),
+ label: t('contacts', 'Last name'),
key: 'lastName'
},
{
- label: t('contacts', 'Display-name'),
+ label: t('contacts', 'Display name'),
key: 'displayName'
}
]
diff --git a/src/services/api.js b/src/services/api.js
new file mode 100644
index 00000000..3453d293
--- /dev/null
+++ b/src/services/api.js
@@ -0,0 +1,127 @@
+/*
+ * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import axios from 'axios'
+
+const requestToken = document.getElementsByTagName('head')[0].getAttribute('data-requesttoken')
+const tokenHeaders = { headers: { requesttoken: requestToken } }
+
+const sanitize = function(url) {
+ return url.replace(/\/$/, '') // Remove last url slash
+}
+
+export default {
+
+ /**
+ * This Promise is used to chain a request that require an admin password confirmation
+ * Since chaining Promise have a very precise behavior concerning catch and then,
+ * you'll need to be careful when using it.
+ * e.g
+ * // store
+ * action(context) {
+ * return api.requireAdmin().then((response) => {
+ * return api.get('url')
+ * .then((response) => {API success})
+ * .catch((error) => {API failure});
+ * }).catch((error) => {requireAdmin failure});
+ * }
+ * // vue
+ * this.$store.dispatch('action').then(() => {always executed})
+ *
+ * Since Promise.then().catch().then() will always execute the last then
+ * this.$store.dispatch('action').then will always be executed
+ *
+ * If you want requireAdmin failure to also catch the API request failure
+ * you will need to throw a new error in the api.get.catch()
+ *
+ * e.g
+ * api.requireAdmin().then((response) => {
+ * api.get('url')
+ * .then((response) => {API success})
+ * .catch((error) => {throw error;});
+ * }).catch((error) => {requireAdmin OR API failure});
+ *
+ * @returns {Promise}
+ */
+ requireAdmin() {
+ return new Promise(function(resolve, reject) {
+ // TODO: migrate the OC.dialog to Vue and avoid this mess
+ // wait for password confirmation
+ let passwordTimeout
+ let waitForpassword = function() {
+ if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
+ passwordTimeout = setTimeout(waitForpassword, 500)
+ return
+ }
+ clearTimeout(passwordTimeout)
+ clearTimeout(promiseTimeout)
+ resolve()
+ }
+
+ // automatically reject after 5s if not resolved
+ let promiseTimeout = setTimeout(() => {
+ clearTimeout(passwordTimeout)
+ // close dialog
+ if (document.getElementsByClassName('oc-dialog-close').length > 0) {
+ document.getElementsByClassName('oc-dialog-close')[0].click()
+ }
+ OC.Notification.showTemporary(t('settings', 'You did not enter the password in time'))
+ reject(new Error('Password request cancelled'))
+ }, 7000)
+
+ // request password
+ OC.PasswordConfirmation.requirePasswordConfirmation()
+ waitForpassword()
+ })
+ },
+ get(url) {
+ return axios.get(sanitize(url), tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error))
+ },
+ post(url, data) {
+ return axios.post(sanitize(url), data, tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error))
+ },
+ patch(url, data) {
+ return axios.patch(sanitize(url), data, tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error))
+ },
+ put(url, data) {
+ return axios.put(sanitize(url), data, tokenHeaders)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error))
+ },
+ delete(url, data) {
+ return axios.delete(sanitize(url), { data: data, headers: tokenHeaders.headers })
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error))
+ },
+ all(promises) {
+ return axios.all(promises)
+ .then((response) => Promise.resolve(response))
+ .catch((error) => Promise.reject(error))
+
+ }
+}
diff --git a/src/store/addressbooks.js b/src/store/addressbooks.js
index 775c4bf9..bb1858f2 100644
--- a/src/store/addressbooks.js
+++ b/src/store/addressbooks.js
@@ -104,21 +104,26 @@ const mutations = {
* @param {Object} state
* @param {Object} data
* @param {Object} data.addressbook the addressbook
+ * @param {String} data.sharee the sharee
+ * @param {Boolean} data.id id
+ * @param {Boolean} data.group group
*/
- shareAddressbook(state, addressbook, sharee) {
- addressbook = state.addressbooks.find(search => search === addressbook)
- let newSharee = {}
- sharee.displayname = sharee
- sharee.writable = false
- addressbook.shares.append(newSharee)
+ shareAddressbook(state, { addressbook, sharee, id, group }) {
+ addressbook = state.addressbooks.find(search => search.id === addressbook.id)
+ let newSharee = {
+ displayname: sharee,
+ id,
+ writeable: false,
+ group
+ }
+ addressbook.shares.push(newSharee)
},
/**
- * Remove Share from addressbook shares list
+ * Remove Sharee from addressbook shares list
*
* @param {Object} state
- * @param {Object} data
- * @param {Object} data.addressbook the addressbook
+ * @param {Object} sharee the sharee
*/
removeSharee(state, sharee) {
let addressbook = state.addressbooks.find(search => {
@@ -128,15 +133,13 @@ const mutations = {
}
}
})
- addressbook.shares.splice(sharee, 1)
+ addressbook.shares.splice(addressbook.shares.indexOf(sharee), 1)
},
/**
* Toggle sharee's writable permission
*
* @param {Object} state
- * @param {Object} data
- * @param {Object} data.addressbook the addressbook
* @param {Object} sharee the sharee
*/
updateShareeWritable(state, sharee) {
@@ -175,9 +178,11 @@ const actions = {
dav: addressbook
}
})
+
addressbooks.forEach(addressbook => {
context.commit('addAddressbooks', addressbook)
})
+
return addressbooks
},
@@ -219,11 +224,14 @@ const actions = {
/**
* Share Adressbook with User or Group
* @param {Object} context Current context
- * @param {Object} addressbook Addressbook selected
- * @param {Object} sharee Addressbook sharee object
+ * @param {Object} data.addressbook the addressbook
+ * @param {String} data.sharee the sharee
+ * @param {Boolean} data.id id
+ * @param {Boolean} data.group group
*/
- shareAddressbook(contect, addressbook, sharee) {
+ shareAddressbook(context, { addressbook, sharee, id, group }) {
// Share addressbook with entered group or user
+ context.commit('shareAddressbook', { addressbook, sharee, id, group })
},
/**