aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/src/components/SharingInput.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/src/components/SharingInput.vue')
-rw-r--r--apps/files_sharing/src/components/SharingInput.vue427
1 files changed, 227 insertions, 200 deletions
diff --git a/apps/files_sharing/src/components/SharingInput.vue b/apps/files_sharing/src/components/SharingInput.vue
index 6ca2299b81c..6fb33aba6b2 100644
--- a/apps/files_sharing/src/components/SharingInput.vue
+++ b/apps/files_sharing/src/components/SharingInput.vue
@@ -1,71 +1,57 @@
<!--
- - @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/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <Multiselect ref="multiselect"
- class="sharing-input"
- :clear-on-select="false"
- :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"
- open-direction="below"
- @search-change="asyncFind"
- @select="addShare">
- <template #noOptions>
- {{ t('files_sharing', 'No recommendations. Start typing.') }}
- </template>
- <template #noResult>
- {{ noResultText }}
- </template>
- </Multiselect>
+ <div class="sharing-search">
+ <label class="hidden-visually" :for="shareInputId">
+ {{ isExternal ? t('files_sharing', 'Enter external recipients')
+ : t('files_sharing', 'Search for internal recipients') }}
+ </label>
+ <NcSelect ref="select"
+ v-model="value"
+ :input-id="shareInputId"
+ class="sharing-search__input"
+ :disabled="!canReshare"
+ :loading="loading"
+ :filterable="false"
+ :placeholder="inputPlaceholder"
+ :clear-search-on-blur="() => false"
+ :user-select="true"
+ :options="options"
+ :label-outside="true"
+ @search="asyncFind"
+ @option:selected="onSelected">
+ <template #no-options="{ search }">
+ {{ search ? noResultText : placeholder }}
+ </template>
+ </NcSelect>
+ </div>
</template>
<script>
import { generateOcsUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
+import { getCapabilities } from '@nextcloud/capabilities'
import axios from '@nextcloud/axios'
import debounce from 'debounce'
-import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
-import Config from '../services/ConfigService'
-import Share from '../models/Share'
-import ShareRequests from '../mixins/ShareRequests'
-import ShareTypes from '../mixins/ShareTypes'
+import Config from '../services/ConfigService.ts'
+import Share from '../models/Share.ts'
+import ShareRequests from '../mixins/ShareRequests.js'
+import ShareDetails from '../mixins/ShareDetails.js'
+import { ShareType } from '@nextcloud/sharing'
export default {
name: 'SharingInput',
components: {
- Multiselect,
+ NcSelect,
},
- mixins: [ShareTypes, ShareRequests],
+ mixins: [ShareRequests, ShareDetails],
props: {
shares: {
@@ -91,6 +77,20 @@ export default {
type: Boolean,
required: true,
},
+ isExternal: {
+ type: Boolean,
+ default: false,
+ },
+ placeholder: {
+ type: String,
+ default: '',
+ },
+ },
+
+ setup() {
+ return {
+ shareInputId: `share-input-${Math.random().toString(36).slice(2, 7)}`,
+ }
},
data() {
@@ -101,6 +101,7 @@ export default {
recommendations: [],
ShareSearch: OCA.Sharing.ShareSearch.state,
suggestions: [],
+ value: null,
}
},
@@ -111,7 +112,7 @@ export default {
* results into the autocomplete dropdown
* Used for the guests app
*
- * @returns {Array}
+ * @return {Array}
*/
externalResults() {
return this.ShareSearch.results
@@ -122,6 +123,10 @@ export default {
if (!this.canReshare) {
return t('files_sharing', 'Resharing is not allowed')
}
+ if (this.placeholder) {
+ return this.placeholder
+ }
+
// We can always search with email addresses for users too
if (!allowRemoteSharing) {
return t('files_sharing', 'Name or email …')
@@ -150,11 +155,19 @@ export default {
},
mounted() {
- this.getRecommendations()
+ if (!this.isExternal) {
+ // We can only recommend users, groups etc for internal shares
+ this.getRecommendations()
+ }
},
methods: {
- async asyncFind(query, id) {
+ onSelected(option) {
+ this.value = null // Reset selected option
+ this.openSharingDetails(option)
+ },
+
+ async asyncFind(query) {
// save current query to check if we display
// recommendations or search results
this.query = query.trim()
@@ -170,53 +183,69 @@ export default {
* Get suggestions
*
* @param {string} search the search query
- * @param {boolean} [lookup=false] search on lookup server
+ * @param {boolean} [lookup] search on lookup server
*/
async getSuggestions(search, lookup = false) {
this.loading = true
- if (OC.getCapabilities().files_sharing.sharee.query_lookup_default === true) {
+ if (getCapabilities().files_sharing.sharee.query_lookup_default === true) {
lookup = true
}
- const shareType = [
- this.SHARE_TYPES.SHARE_TYPE_USER,
- this.SHARE_TYPES.SHARE_TYPE_GROUP,
- this.SHARE_TYPES.SHARE_TYPE_REMOTE,
- this.SHARE_TYPES.SHARE_TYPE_REMOTE_GROUP,
- this.SHARE_TYPES.SHARE_TYPE_CIRCLE,
- this.SHARE_TYPES.SHARE_TYPE_ROOM,
- this.SHARE_TYPES.SHARE_TYPE_GUEST,
- this.SHARE_TYPES.SHARE_TYPE_DECK,
- ]
-
- if (OC.getCapabilities().files_sharing.public.enabled === true) {
- shareType.push(this.SHARE_TYPES.SHARE_TYPE_EMAIL)
+ const remoteTypes = [ShareType.Remote, ShareType.RemoteGroup]
+ const shareType = []
+
+ const showFederatedAsInternal = this.config.showFederatedSharesAsInternal
+ || this.config.showFederatedSharesToTrustedServersAsInternal
+
+ // For internal users, add remote types if config says to show them as internal
+ const shouldAddRemoteTypes = (!this.isExternal && showFederatedAsInternal)
+ // For external users, add them if config *doesn't* say to show them as internal
+ || (this.isExternal && !showFederatedAsInternal)
+ // Edge case: federated-to-trusted is a separate "add" trigger for external users
+ || (this.isExternal && this.config.showFederatedSharesToTrustedServersAsInternal)
+
+ if (this.isExternal) {
+ if (getCapabilities().files_sharing.public.enabled === true) {
+ shareType.push(ShareType.Email)
+ }
+ } else {
+ shareType.push(
+ ShareType.User,
+ ShareType.Group,
+ ShareType.Team,
+ ShareType.Room,
+ ShareType.Guest,
+ ShareType.Deck,
+ ShareType.ScienceMesh,
+ )
}
- 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,
- shareType,
- },
- })
+ if (shouldAddRemoteTypes) {
+ shareType.push(...remoteTypes)
+ }
- if (request.data.ocs.meta.statuscode !== 100) {
- console.error('Error fetching suggestions', request)
+ let request = null
+ try {
+ 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,
+ shareType,
+ },
+ })
+ } catch (error) {
+ console.error('Error fetching suggestions', error)
return
}
- const data = request.data.ocs.data
- const exact = request.data.ocs.data.exact
- data.exact = [] // removing exact from general results
-
+ const { exact, ...data } = request.data.ocs.data
// 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), [])
+ const rawExactSuggestions = Object.values(exact).flat()
+ const rawSuggestions = Object.values(data).flat()
// remove invalid data and format to user-select layout
const exactSuggestions = this.filterOutExistingShares(rawExactSuggestions)
@@ -233,8 +262,9 @@ export default {
const lookupEntry = []
if (data.lookupEnabled && !lookup) {
lookupEntry.push({
+ id: 'global-lookup',
isNoUser: true,
- displayName: t('files_sharing', 'Search globally'),
+ displayName: t('files_sharing', 'Search everywhere'),
lookup: true,
})
}
@@ -244,7 +274,7 @@ export default {
const allSuggestions = exactSuggestions.concat(suggestions).concat(externalResults).concat(lookupEntry)
- // Count occurances of display names in order to provide a distinguishable description if needed
+ // Count occurrences of display names in order to provide a distinguishable description if needed
const nameCounts = allSuggestions.reduce((nameCounts, result) => {
if (!result.displayName) {
return nameCounts
@@ -283,24 +313,25 @@ export default {
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)
+ let request = null
+ try {
+ request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees_recommended'), {
+ params: {
+ format: 'json',
+ itemType: this.fileInfo.type,
+ },
+ })
+ } catch (error) {
+ console.error('Error fetching recommendations', error)
return
}
+ // Add external results from the OCA.Sharing.ShareSearch api
const externalResults = this.externalResults.filter(result => !result.condition || result.condition(this))
- const exact = request.data.ocs.data.exact
-
// flatten array of arrays
- const rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
+ const rawRecommendations = Object.values(request.data.ocs.data.exact)
+ .reduce((arr, elem) => arr.concat(elem), [])
// remove invalid data and format to user-select layout
this.recommendations = this.filterOutExistingShares(rawRecommendations)
@@ -315,8 +346,8 @@ export default {
* Filter out existing shares from
* the provided shares search results
*
- * @param {Object[]} shares the array of shares object
- * @returns {Object[]}
+ * @param {object[]} shares the array of shares object
+ * @return {object[]}
*/
filterOutExistingShares(shares) {
return shares.reduce((arr, share) => {
@@ -325,7 +356,7 @@ export default {
return arr
}
try {
- if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_USER) {
+ if (share.value.shareType === ShareType.User) {
// filter out current user
if (share.value.shareWith === getCurrentUser().uid) {
return arr
@@ -338,7 +369,12 @@ export default {
}
// filter out existing mail shares
- if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_EMAIL) {
+ if (share.value.shareType === ShareType.Email) {
+ // When sharing internally, we don't want to suggest email addresses
+ // that the user previously created shares to
+ if (!this.isExternal) {
+ return arr
+ }
const emails = this.linkShares.map(elem => elem.shareWith)
if (emails.indexOf(share.value.shareWith.trim()) !== -1) {
return arr
@@ -370,110 +406,91 @@ export default {
/**
* Get the icon based on the share type
+ *
* @param {number} type the share type
- * @returns {string} the icon class
+ * @return {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
+ case ShareType.Guest:
+ // default is a user, other icons are here to differentiate
// 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'
- case this.SHARE_TYPES.SHARE_TYPE_DECK:
- return 'icon-deck'
-
+ // case ShareType.Remote:
+ // case ShareType.User:
+ return {
+ icon: 'icon-user',
+ iconTitle: t('files_sharing', 'Guest'),
+ }
+ case ShareType.RemoteGroup:
+ case ShareType.Group:
+ return {
+ icon: 'icon-group',
+ iconTitle: t('files_sharing', 'Group'),
+ }
+ case ShareType.Email:
+ return {
+ icon: 'icon-mail',
+ iconTitle: t('files_sharing', 'Email'),
+ }
+ case ShareType.Team:
+ return {
+ icon: 'icon-teams',
+ iconTitle: t('files_sharing', 'Team'),
+ }
+ case ShareType.Room:
+ return {
+ icon: 'icon-room',
+ iconTitle: t('files_sharing', 'Talk conversation'),
+ }
+ case ShareType.Deck:
+ return {
+ icon: 'icon-deck',
+ iconTitle: t('files_sharing', 'Deck board'),
+ }
+ case ShareType.Sciencemesh:
+ return {
+ icon: 'icon-sciencemesh',
+ iconTitle: t('files_sharing', 'ScienceMesh'),
+ }
default:
- return ''
+ return {}
}
},
/**
* Format shares for the multiselect options
- * @param {Object} result select entry item
- * @returns {Object}
+ *
+ * @param {object} result select entry item
+ * @return {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
+ let subname
+ let displayName = result.name || result.label
+
+ if (result.value.shareType === ShareType.User && this.config.shouldAlwaysShowUnique) {
+ subname = result.shareWithDisplayNameUnique ?? ''
+ } else if (result.value.shareType === ShareType.Email) {
+ subname = result.value.shareWith
+ } else if (result.value.shareType === ShareType.Remote || result.value.shareType === ShareType.RemoteGroup) {
+ if (this.config.showFederatedSharesAsInternal) {
+ subname = result.extra?.email?.value ?? ''
+ displayName = result.extra?.name?.value ?? displayName
+ } else if (result.value.server) {
+ subname = t('files_sharing', 'on {server}', { server: result.value.server })
+ }
} else {
- desc = result.shareWithDescription ?? ''
+ subname = result.shareWithDescription ?? ''
}
return {
shareWith: result.value.shareWith,
shareType: result.value.shareType,
user: result.uuid || result.value.shareWith,
- isNoUser: result.value.shareType !== this.SHARE_TYPES.SHARE_TYPE_USER,
- displayName: result.name || result.label,
- desc,
+ isNoUser: result.value.shareType !== ShareType.User,
+ displayName,
+ subname,
shareWithDisplayNameUnique: result.shareWithDisplayNameUnique || '',
- icon: this.shareTypeToIcon(result.value.shareType),
- }
- },
-
- /**
- * Process the new share request
- * @param {Object} value the multiselect option
- */
- async addShare(value) {
- if (value.lookup) {
- await this.getSuggestions(this.query, true)
-
- // focus the input again
- this.$nextTick(() => {
- this.$refs.multiselect.$el.querySelector('.multiselect__input').focus()
- })
- return true
- }
-
- // TODO: reset the search string when done
- // https://github.com/shentao/vue-multiselect/issues/633
-
- // 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,
- permissions: this.fileInfo.sharePermissions & OC.getCapabilities().files_sharing.default_permissions,
- })
- 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
+ ...this.shareTypeToIcon(result.value.shareType),
}
},
},
@@ -481,21 +498,31 @@ export default {
</script>
<style lang="scss">
-.sharing-input {
- width: 100%;
- margin: 10px 0;
+.sharing-search {
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 4px;
+
+ label[for="sharing-search-input"] {
+ margin-bottom: 2px;
+ }
+ &__input {
+ width: 100%;
+ margin: 10px 0;
+ }
+}
+
+.vs__dropdown-menu {
// 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;
- }
+ span[lookup] {
+ .avatardiv {
+ background-image: var(--icon-search-white);
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: var(--color-text-maxcontrast) !important;
+ .avatardiv__initials-wrapper {
+ display: none;
}
}
}