diff options
author | Jessica Greene <jessica0greene@gmail.com> | 2018-08-28 16:05:37 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-08-28 16:05:37 +0200 |
commit | d0517b8a42662f7323fa54f09b1eb85377be90d2 (patch) | |
tree | 96557d051ea31f1a71f492976385532335243a81 /src | |
parent | 670493bd8a029d8e3b6ab75778cb2fd0cb16378a (diff) | |
parent | ea5973c1a58c2dcca8bafd94e6233aac96155e18 (diff) |
Merge pull request #595 from nextcloud/vue-share-addressbook
Vue share addressbook 🌮
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Settings/SettingsAddressbook.vue | 2 | ||||
-rw-r--r-- | src/components/Settings/SettingsAddressbookShare.vue | 172 | ||||
-rw-r--r-- | src/components/Settings/SettingsAddressbookSharee.vue | 3 | ||||
-rw-r--r-- | src/components/Settings/SettingsNewAddressbook.vue | 9 | ||||
-rw-r--r-- | src/components/Settings/SettingsSortContacts.vue | 4 | ||||
-rw-r--r-- | src/services/api.js | 127 | ||||
-rw-r--r-- | src/store/addressbooks.js | 38 |
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 }) }, /** |