aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/components/SharingInput.vue
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2019-05-23 17:03:04 +0200
committerDaniel Calviño Sánchez <danxuliu@gmail.com>2019-10-29 12:56:00 +0100
commitfd90af50d910e659aa8df0d380424383c6c09620 (patch)
tree76d8ddcc7cf44ba6852f31b0a2323d23d6b1c258 /apps/files_sharing/src/components/SharingInput.vue
parentea6f423e2c8e50cf1357a0e2182dc4c9a9bf981e (diff)
downloadnextcloud-server-fd90af50d910e659aa8df0d380424383c6c09620.tar.gz
nextcloud-server-fd90af50d910e659aa8df0d380424383c6c09620.zip
Add OCA.Files.Sidebar
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files_sharing/src/components/SharingInput.vue')
-rw-r--r--apps/files_sharing/src/components/SharingInput.vue444
1 files changed, 444 insertions, 0 deletions
diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue
new file mode 100644
index 00000000000..df222eafe0c
--- /dev/null
+++ b/apps/files_sharing/src/components/SharingInput.vue
@@ -0,0 +1,444 @@
+<!--
+ - @copyright Copyright (c) 2019 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/>.
+ -
+ -->
+
+<template>
+ <Multiselect ref="multiselect"
+ class="sharing-input"
+ :disabled="!canReshare"
+ :hide-selected="true"
+ :internal-search="false"
+ :loading="loading"
+ :options="options"
+ :placeholder="inputPlaceholder"
+ :preselect-first="true"
+ :preserve-search="true"
+ :searchable="true"
+ :user-select="true"
+ @search-change="asyncFind"
+ @select="addShare">
+ <template #noOptions>
+ {{ t('files_sharing', 'No recommendations. Start typing.') }}
+ </template>
+ <template #noResult>
+ {{ noResultText }}
+ </template>
+ </Multiselect>
+</template>
+
+<script>
+import { generateOcsUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import axios from '@nextcloud/axios'
+import debounce from 'debounce'
+import Multiselect from 'nextcloud-vue/dist/Components/Multiselect'
+
+import Config from '../services/ConfigService'
+import Share from '../models/Share'
+import ShareRequests from '../mixins/ShareRequests'
+import ShareTypes from '../mixins/ShareTypes'
+
+export default {
+ name: 'SharingInput',
+
+ components: {
+ Multiselect
+ },
+
+ mixins: [ShareTypes, ShareRequests],
+
+ props: {
+ shares: {
+ type: Array,
+ default: () => [],
+ required: true
+ },
+ linkShares: {
+ type: Array,
+ default: () => [],
+ required: true
+ },
+ fileInfo: {
+ type: Object,
+ default: () => {},
+ required: true
+ },
+ reshare: {
+ type: Share,
+ default: null
+ },
+ canReshare: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ config: new Config(),
+ loading: false,
+ query: '',
+ recommendations: [],
+ ShareSearch: OCA.Sharing.ShareSearch.state,
+ suggestions: []
+ }
+ },
+
+ computed: {
+ /**
+ * Implement ShareSearch
+ * allows external appas to inject new
+ * results into the autocomplete dropdown
+ * Used for the guests app
+ *
+ * @returns {Array}
+ */
+ externalResults() {
+ return this.ShareSearch.results
+ },
+ inputPlaceholder() {
+ const allowRemoteSharing = this.config.isRemoteShareAllowed
+ const allowMailSharing = this.config.isMailShareAllowed
+
+ if (!this.canReshare) {
+ return t('files_sharing', 'Resharing is not allowed')
+ }
+ if (!allowRemoteSharing && allowMailSharing) {
+ return t('files_sharing', 'Name or email address...')
+ }
+ if (allowRemoteSharing && !allowMailSharing) {
+ return t('files_sharing', 'Name or federated cloud ID...')
+ }
+ if (allowRemoteSharing && allowMailSharing) {
+ return t('files_sharing', 'Name, federated cloud ID or email address...')
+ }
+
+ return t('files_sharing', 'Name...')
+ },
+
+ isValidQuery() {
+ return this.query && this.query.trim() !== '' && this.query.length > this.config.minSearchStringLength
+ },
+
+ options() {
+ if (this.isValidQuery) {
+ return this.suggestions
+ }
+ return this.recommendations
+ },
+
+ noResultText() {
+ if (this.loading) {
+ return t('files_sharing', 'Searching...')
+ }
+ return t('files_sharing', 'No elements found.')
+ }
+ },
+
+ mounted() {
+ this.getRecommendations()
+ },
+
+ methods: {
+ async asyncFind(query, id) {
+ // save current query to check if we display
+ // recommendations or search results
+ this.query = query.trim()
+ if (this.isValidQuery) {
+ // start loading now to have proper ux feedback
+ // during the debounce
+ this.loading = true
+ await this.debounceGetSuggestions(query)
+ }
+ },
+
+ /**
+ * Get suggestions
+ *
+ * @param {string} search the search query
+ * @param {boolean} [lookup=false] search on lookup server
+ */
+ async getSuggestions(search, lookup) {
+ this.loading = true
+ lookup = lookup || false
+ console.info(search, lookup)
+
+ const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', {
+ params: {
+ format: 'json',
+ itemType: this.fileInfo.type === 'dir' ? 'folder' : 'file',
+ search,
+ lookup,
+ perPage: this.config.maxAutocompleteResults
+ }
+ })
+
+ if (request.data.ocs.meta.statuscode !== 100) {
+ console.error('Error fetching suggestions', request)
+ return
+ }
+
+ const data = request.data.ocs.data
+ const exact = request.data.ocs.data.exact
+ data.exact = [] // removing exact from general results
+
+ // flatten array of arrays
+ const rawExactSuggestions = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
+ const rawSuggestions = Object.values(data).reduce((arr, elem) => arr.concat(elem), [])
+
+ // remove invalid data and format to user-select layout
+ const exactSuggestions = this.filterOutExistingShares(rawExactSuggestions)
+ .map(share => this.formatForMultiselect(share))
+ const suggestions = this.filterOutExistingShares(rawSuggestions)
+ .map(share => this.formatForMultiselect(share))
+
+ // lookup clickable entry
+ const lookupEntry = []
+ if (data.lookupEnabled) {
+ lookupEntry.push({
+ isNoUser: true,
+ displayName: t('files_sharing', 'Search globally'),
+ lookup: true
+ })
+ }
+
+ // if there is a condition specified, filter it
+ const externalResults = this.externalResults.filter(result => !result.condition || result.condition(this))
+
+ this.suggestions = exactSuggestions.concat(suggestions).concat(externalResults).concat(lookupEntry)
+
+ this.loading = false
+ console.info('suggestions', this.suggestions)
+ },
+
+ /**
+ * Debounce getSuggestions
+ *
+ * @param {...*} args the arguments
+ */
+ debounceGetSuggestions: debounce(function(...args) {
+ this.getSuggestions(...args)
+ }, 300),
+
+ /**
+ * Get the sharing recommendations
+ */
+ async getRecommendations() {
+ this.loading = true
+
+ const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees_recommended', {
+ params: {
+ format: 'json',
+ itemType: this.fileInfo.type
+ }
+ })
+
+ if (request.data.ocs.meta.statuscode !== 100) {
+ console.error('Error fetching recommendations', request)
+ return
+ }
+
+ const exact = request.data.ocs.data.exact
+
+ // flatten array of arrays
+ const rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
+
+ // remove invalid data and format to user-select layout
+ this.recommendations = this.filterOutExistingShares(rawRecommendations)
+ .map(share => this.formatForMultiselect(share))
+
+ this.loading = false
+ console.info('recommendations', this.recommendations)
+ },
+
+ /**
+ * Filter out existing shares from
+ * the provided shares search results
+ *
+ * @param {Object[]} shares the array of shares object
+ * @returns {Object[]}
+ */
+ filterOutExistingShares(shares) {
+ return shares.reduce((arr, share) => {
+ // only check proper objects
+ if (typeof share !== 'object') {
+ return arr
+ }
+ try {
+ // filter out current user
+ if (share.value.shareWith === getCurrentUser().uid) {
+ return arr
+ }
+
+ // filter out the owner of the share
+ if (this.reshare && share.value.shareWith === this.reshare.owner) {
+ return arr
+ }
+
+ // filter out existing mail shares
+ if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
+ const emails = this.linkShares.map(elem => elem.shareWith)
+ if (emails.indexOf(share.value.shareWith.trim()) !== -1) {
+ return arr
+ }
+ } else { // filter out existing shares
+ // creating an object of uid => type
+ const sharesObj = this.shares.reduce((obj, elem) => {
+ obj[elem.shareWith] = elem.type
+ return obj
+ }, {})
+
+ // if shareWith is the same and the share type too, ignore it
+ const key = share.value.shareWith.trim()
+ if (key in sharesObj
+ && sharesObj[key] === share.value.shareType) {
+ return arr
+ }
+ }
+
+ // ALL GOOD
+ // let's add the suggestion
+ arr.push(share)
+ } catch {
+ return arr
+ }
+ return arr
+ }, [])
+ },
+
+ /**
+ * Get the icon based on the share type
+ * @param {number} type the share type
+ * @returns {string} the icon class
+ */
+ shareTypeToIcon(type) {
+ switch (type) {
+ case this.SHARE_TYPES.SHARE_TYPE_GUEST:
+ // default is a user, other icons are here to differenciate
+ // themselves from it, so let's not display the user icon
+ // case this.SHARE_TYPES.SHARE_TYPE_REMOTE:
+ // case this.SHARE_TYPES.SHARE_TYPE_USER:
+ return 'icon-user'
+ case this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP:
+ case this.SHARE_TYPES.SHARE_TYPE_GROUP:
+ return 'icon-group'
+ case this.SHARE_TYPES.SHARE_TYPE_EMAIL:
+ return 'icon-mail'
+ case this.SHARE_TYPES.SHARE_TYPE_CIRCLE:
+ return 'icon-circle'
+ case this.SHARE_TYPES.SHARE_TYPE_ROOM:
+ return 'icon-room'
+
+ default:
+ return ''
+ }
+ },
+
+ /**
+ * Format shares for the multiselect options
+ * @param {Object} result select entry item
+ * @returns {Object}
+ */
+ formatForMultiselect(result) {
+ let desc
+ if ((result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE
+ || result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP
+ ) && result.value.server) {
+ desc = t('files_sharing', 'on {server}', { server: result.value.server })
+ } else if (result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
+ desc = result.value.shareWith
+ }
+
+ return {
+ shareWith: result.value.shareWith,
+ shareType: result.value.shareType,
+ user: result.uuid || result.value.shareWith,
+ isNoUser: !result.uuid,
+ displayName: result.name || result.label,
+ desc,
+ icon: this.shareTypeToIcon(result.value.shareType)
+ }
+ },
+
+ /**
+ * Process the new share request
+ * @param {Object} value the multiselect option
+ */
+ async addShare(value) {
+ if (value.lookup) {
+ return this.getSuggestions(this.query, true)
+ }
+
+ // handle externalResults from OCA.Sharing.ShareSearch
+ if (value.handler) {
+ const share = await value.handler(this)
+ this.$emit('add:share', new Share(share))
+ return true
+ }
+
+ this.loading = true
+ try {
+ const path = (this.fileInfo.path + '/' + this.fileInfo.name).replace('//', '/')
+ const share = await this.createShare({
+ path,
+ shareType: value.shareType,
+ shareWith: value.shareWith
+ })
+ this.$emit('add:share', share)
+
+ this.getRecommendations()
+
+ } catch (response) {
+ // focus back if any error
+ const input = this.$refs.multiselect.$el.querySelector('input')
+ if (input) {
+ input.focus()
+ }
+ this.query = value.shareWith
+ } finally {
+ this.loading = false
+ }
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+.sharing-input {
+ width: 100%;
+ margin: 10px 0;
+
+ // properly style the lookup entry
+ .multiselect__option {
+ span[lookup] {
+ .avatardiv {
+ background-image: var(--icon-search-fff);
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--color-text-maxcontrast) !important;
+ div {
+ display: none;
+ }
+ }
+ }
+ }
+}
+</style>