mirror of
https://github.com/nextcloud/tables.git
synced 2025-07-24 12:20:38 +00:00
Merge pull request #1025 from nextcloud/consolidate-ncselect
enh: Consolidate user/group search code
This commit is contained in:
@ -99,7 +99,7 @@ describe('Manage a table', () => {
|
||||
cy.get('[data-cy="editTableModal"]').should('not.exist')
|
||||
cy.get('[data-cy="transferTableModal"]').should('be.visible')
|
||||
cy.get('[data-cy="transferTableModal"] input[type="search"]').clear().type(targetUserTransfer.userId)
|
||||
cy.get(`.vs__dropdown-menu [user="${targetUserTransfer.userId}"]`).click()
|
||||
cy.get(`.vs__dropdown-menu [id="${targetUserTransfer.userId}"]`).click()
|
||||
cy.get('[data-cy="transferTableButton"]').should('be.enabled').click()
|
||||
cy.get('.toastify.toast-success').should('be.visible')
|
||||
cy.get('.app-navigation__list').contains('test table').should('not.exist')
|
||||
|
@ -186,7 +186,7 @@ export default {
|
||||
sharing = sharing.filter((share) => getCurrentUser().uid !== share.receiver)
|
||||
const receivers = sharing.map((share) => {
|
||||
return {
|
||||
user: share.receiver,
|
||||
id: share.receiver,
|
||||
displayName: share.receiver_display_name,
|
||||
icon: share.receiver_type === 'user' ? 'icon-user' : 'icon-group',
|
||||
isUser: share.receiver_type === 'user',
|
||||
|
@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3>{{ t('tables', 'Transfer this application to another user') }}</h3>
|
||||
<NcUserAndGroupPicker :select-users="true" :select-groups="false" :new-owner-user-id.sync="newOwnerId" />
|
||||
<NcUserPicker :select-users="true" :select-groups="false" :selected-user-id.sync="newOwnerId" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="fix-col-4 space-T end">
|
||||
@ -28,7 +28,7 @@ import { NcModal, NcButton } from '@nextcloud/vue'
|
||||
import { showSuccess } from '@nextcloud/dialogs'
|
||||
import '@nextcloud/dialogs/dist/index.css'
|
||||
import permissionsMixin from '../../shared/components/ncTable/mixins/permissionsMixin.js'
|
||||
import NcUserAndGroupPicker from '../../shared/components/ncUserAndGroupPicker/NcUserAndGroupPicker.vue'
|
||||
import NcUserPicker from '../../shared/components/ncUserPicker/NcUserPicker.vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
@ -37,7 +37,7 @@ export default {
|
||||
components: {
|
||||
NcModal,
|
||||
NcButton,
|
||||
NcUserAndGroupPicker,
|
||||
NcUserPicker,
|
||||
},
|
||||
mixins: [permissionsMixin],
|
||||
props: {
|
||||
|
@ -10,11 +10,11 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3>{{ t('tables', 'Transfer this table to another user') }}</h3>
|
||||
<NcUserAndGroupPicker :select-users="true" :select-groups="false" :new-owner-user-id.sync="newOwnerUserId" />
|
||||
<NcUserPicker :select-users="true" :select-groups="false" :selected-user-id.sync="selectedUserId" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="fix-col-4 space-T end">
|
||||
<NcButton type="warning" :disabled="newOwnerUserId === ''" data-cy="transferTableButton" @click="transferMe">
|
||||
<NcButton type="warning" :disabled="selectedUserId === ''" data-cy="transferTableButton" @click="transferMe">
|
||||
{{ t('tables', 'Transfer') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
@ -28,7 +28,7 @@ import { NcModal, NcButton } from '@nextcloud/vue'
|
||||
import { showSuccess } from '@nextcloud/dialogs'
|
||||
import '@nextcloud/dialogs/dist/index.css'
|
||||
import permissionsMixin from '../../shared/components/ncTable/mixins/permissionsMixin.js'
|
||||
import NcUserAndGroupPicker from '../../shared/components/ncUserAndGroupPicker/NcUserAndGroupPicker.vue'
|
||||
import NcUserPicker from '../../shared/components/ncUserPicker/NcUserPicker.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
@ -37,7 +37,7 @@ export default {
|
||||
components: {
|
||||
NcModal,
|
||||
NcButton,
|
||||
NcUserAndGroupPicker,
|
||||
NcUserPicker,
|
||||
},
|
||||
mixins: [permissionsMixin],
|
||||
props: {
|
||||
@ -53,7 +53,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
newOwnerUserId: '',
|
||||
selectedUserId: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -84,9 +84,9 @@ export default {
|
||||
if (this.activeTable) {
|
||||
activeTableId = !this.isView ? this.activeTable.id : null
|
||||
}
|
||||
const res = await this.$store.dispatch('transferTable', { id: this.table.id, data: { newOwnerUserId: this.newOwnerUserId } })
|
||||
const res = await this.$store.dispatch('transferTable', { id: this.table.id, data: { newOwnerUserId: this.selectedUserId } })
|
||||
if (res) {
|
||||
showSuccess(t('tables', 'Table "{emoji}{table}" transferred to {user}', { emoji: this.table?.emoji ? this.table?.emoji + ' ' : '', table: this.table?.title, user: this.newOwnerUserId }))
|
||||
showSuccess(t('tables', 'Table "{emoji}{table}" transferred to {user}', { emoji: this.table?.emoji ? this.table?.emoji + ' ' : '', table: this.table?.title, user: this.selectedUserId }))
|
||||
|
||||
if (transferId === activeTableId) {
|
||||
await this.$router.push('/').catch(err => err)
|
||||
|
@ -25,23 +25,11 @@
|
||||
<template>
|
||||
<div class="row space-B">
|
||||
<h3>{{ t('tables', 'Share with accounts or groups') }}</h3>
|
||||
<NcSelect id="ajax"
|
||||
style="width: 100%;"
|
||||
:clear-on-select="true"
|
||||
:hide-selected="true"
|
||||
:internal-search="false"
|
||||
:loading="loading"
|
||||
:options="options"
|
||||
:placeholder="t('tables', 'User or group name …')"
|
||||
:preselect-first="true"
|
||||
:preserve-search="true"
|
||||
:searchable="true"
|
||||
:user-select="true"
|
||||
:get-option-key="(option) => option.key"
|
||||
:aria-label-combobox="t('tables', 'User or group name …')"
|
||||
label="displayName"
|
||||
@search="asyncFind"
|
||||
@input="addShare">
|
||||
<NcSelect id="ajax" style="width: 100%;" :clear-on-select="true" :hide-selected="true" :internal-search="false"
|
||||
:loading="loading" :options="options" :placeholder="t('tables', 'User or group name …')"
|
||||
:preselect-first="true" :preserve-search="true" :searchable="true" :user-select="true"
|
||||
:get-option-key="(option) => option.key" :aria-label-combobox="t('tables', 'User or group name …')"
|
||||
label="displayName" @search="asyncFind" @input="addShare">
|
||||
<template #no-options>
|
||||
{{ t('tables', 'No recommendations. Start typing.') }}
|
||||
</template>
|
||||
@ -54,13 +42,13 @@
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import { NcSelect } from '@nextcloud/vue'
|
||||
import { mapState } from 'vuex'
|
||||
import formatting from '../../../shared/mixins/formatting.js'
|
||||
import ShareTypes from '../../../shared/mixins/shareTypesMixin.js'
|
||||
import searchUserGroup from '../../../shared/mixins/searchUserGroup.js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
export default {
|
||||
name: 'ShareForm',
|
||||
@ -68,66 +56,25 @@ export default {
|
||||
NcSelect,
|
||||
},
|
||||
|
||||
mixins: [ShareTypes, formatting],
|
||||
mixins: [formatting, ShareTypes, searchUserGroup],
|
||||
|
||||
props: {
|
||||
shares: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
loading: false,
|
||||
|
||||
minSearchStringLength: 1,
|
||||
maxAutocompleteResults: 20,
|
||||
|
||||
// Search data
|
||||
recommendations: [],
|
||||
suggestions: [],
|
||||
}
|
||||
selectUsers: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
selectGroups: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['tables', 'tablesLoading', 'showSidebar']),
|
||||
|
||||
/**
|
||||
* Is the search valid ?
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isValidQuery() {
|
||||
return this.query && this.query.trim() !== '' && this.query.length > this.minSearchStringLength
|
||||
},
|
||||
|
||||
/**
|
||||
* Multiselect options. Recommendations by default,
|
||||
* direct search when search query is valid.
|
||||
* Filter out existing shares
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
options() {
|
||||
const shareTypes = { 0: 'user', 1: 'group' }
|
||||
// const shares = [...this.userShares, ...this.groupShares]
|
||||
const shares = this.shares
|
||||
if (this.isValidQuery) {
|
||||
// Filter out existing shares
|
||||
return this.suggestions.filter(item => !shares.find(share => share.receiver === item.shareWith && share.receiverType === shareTypes[item.shareType]))
|
||||
}
|
||||
// Filter out existing shares
|
||||
return this.recommendations.filter(item => !shares.find(share => share.receiver === item.shareWith && share.receiverType === shareTypes[item.shareType]))
|
||||
},
|
||||
|
||||
noResultText() {
|
||||
if (this.loading) {
|
||||
return t('tables', 'Searching …')
|
||||
}
|
||||
return t('tables', 'No elements found.')
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@ -139,128 +86,61 @@ export default {
|
||||
this.$emit('add', share)
|
||||
},
|
||||
|
||||
async asyncFind(query) {
|
||||
// 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
|
||||
*/
|
||||
async getSuggestions(search) {
|
||||
this.loading = true
|
||||
|
||||
const shareType = [
|
||||
this.SHARE_TYPES.SHARE_TYPE_USER,
|
||||
this.SHARE_TYPES.SHARE_TYPE_GROUP,
|
||||
]
|
||||
|
||||
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {
|
||||
params: {
|
||||
format: 'json',
|
||||
itemType: 'file',
|
||||
search,
|
||||
perPage: this.maxAutocompleteResults,
|
||||
shareType,
|
||||
},
|
||||
})
|
||||
|
||||
const data = request.data.ocs.data
|
||||
const exact = 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.filterOutUnwantedShares(rawExactSuggestions)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
// sort by type so we can get user&groups first...
|
||||
.sort((a, b) => a.shareType - b.shareType)
|
||||
const suggestions = this.filterOutUnwantedShares(rawSuggestions)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
// sort by type so we can get user&groups first...
|
||||
.sort((a, b) => a.shareType - b.shareType)
|
||||
|
||||
this.suggestions = exactSuggestions.concat(suggestions)
|
||||
|
||||
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: 'file',
|
||||
},
|
||||
})
|
||||
try {
|
||||
const request = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees_recommended'), {
|
||||
params: {
|
||||
format: 'json',
|
||||
itemType: 'file',
|
||||
},
|
||||
})
|
||||
|
||||
const exact = request.data.ocs.data.exact
|
||||
const exact = request.data.ocs.data.exact
|
||||
|
||||
// flatten array of arrays
|
||||
const rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||
// flatten array of arrays
|
||||
let rawRecommendations = Object.values(exact).reduce((arr, elem) => arr.concat(elem), [])
|
||||
console.info('recommendations', rawRecommendations)
|
||||
|
||||
// remove invalid data and format to user-select layout
|
||||
this.recommendations = this.filterOutUnwantedShares(rawRecommendations)
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
|
||||
this.loading = false
|
||||
console.info('recommendations', this.recommendations)
|
||||
rawRecommendations = rawRecommendations.map(result => {
|
||||
return this.formatRecommendations(result)
|
||||
})
|
||||
this.recommendations = this.filterOutUnwantedItems(rawRecommendations)
|
||||
this.loading = false
|
||||
} catch (err) {
|
||||
console.debug(err)
|
||||
showError(t('tables', 'Failed to fetch share recommendations'))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter out unwanted shares
|
||||
*
|
||||
* @param {object[]} shares the array of shares objects
|
||||
* @return {object}
|
||||
*/
|
||||
filterOutUnwantedShares(shares) {
|
||||
return shares.reduce((arr, share) => {
|
||||
// only check proper objects
|
||||
if (typeof share !== 'object') {
|
||||
return arr
|
||||
}
|
||||
filterOutUnwantedItems(items) {
|
||||
const shareTypesList = this.getShareTypes()
|
||||
const shareTypes = { 0: 'user', 1: 'group' }
|
||||
|
||||
try {
|
||||
// filter out current user
|
||||
if (share.value.shareType === this.SHARE_TYPES.SHARE_TYPE_USER
|
||||
&& share.value.shareWith === getCurrentUser().uid) {
|
||||
return arr
|
||||
}
|
||||
// Filter out current user and sort
|
||||
items = items.filter((item) => !(item.shareType === this.SHARE_TYPES.SHARE_TYPE_USER && item.shareWith === this.currentUserId)).sort((a, b) => a.shareType - b.shareType)
|
||||
|
||||
// ALL GOOD
|
||||
// let's add the suggestion
|
||||
arr.push(share)
|
||||
} catch {
|
||||
return arr
|
||||
}
|
||||
return arr
|
||||
}, [])
|
||||
// Filter out non-valid share types
|
||||
items = items.filter((item) => (shareTypesList.includes(item.shareType)))
|
||||
|
||||
// Filter out existing shares
|
||||
return items.filter(item => !this.shares.find(share => share.receiver === item.shareWith && share.receiverType === shareTypes[item.shareType]))
|
||||
},
|
||||
|
||||
formatResult(result) {
|
||||
return {
|
||||
shareWith: result.id,
|
||||
shareType: result.source.startsWith('users') ? this.SHARE_TYPES.SHARE_TYPE_USER : this.SHARE_TYPES.SHARE_TYPE_GROUP,
|
||||
user: result.id,
|
||||
isNoUser: !result.source.startsWith('users'),
|
||||
displayName: result.label,
|
||||
icon: result.icon || result.source.startsWith('users') ? 'icon-user' : 'icon-group',
|
||||
key: result.source + '-' + result.id,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -269,57 +149,26 @@ export default {
|
||||
* @param {object} result select entry item
|
||||
* @return {object}
|
||||
*/
|
||||
formatForMultiselect(result) {
|
||||
formatRecommendations(result) {
|
||||
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,
|
||||
icon: this.shareTypeToIcon(result.value.shareType),
|
||||
icon: result.value.shareType === this.SHARE_TYPES.SHARE_TYPE_USER ? 'icon-user' : 'icon-group',
|
||||
// Vue unique binding to render within Multiselect's AvatarSelectOption
|
||||
key: result.uuid || result.value.shareWith + '-' + result.value.shareType + '-' + result.name || result.label,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the icon based on the share type
|
||||
*
|
||||
* @param {number} type the share type
|
||||
* @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
|
||||
// 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 ''
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.multiselect {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -19,14 +19,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import { NcSelect } from '@nextcloud/vue'
|
||||
import formatting from '../../../shared/mixins/formatting.js'
|
||||
import ShareTypes from '../../mixins/shareTypesMixin.js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import searchUserGroup from '../../../shared/mixins/searchUserGroup.js'
|
||||
import '@nextcloud/dialogs/style.css'
|
||||
|
||||
export default {
|
||||
@ -35,7 +30,7 @@ export default {
|
||||
NcSelect,
|
||||
},
|
||||
|
||||
mixins: [ShareTypes, formatting],
|
||||
mixins: [formatting, searchUserGroup],
|
||||
|
||||
props: {
|
||||
receivers: {
|
||||
@ -54,14 +49,8 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
value: '',
|
||||
loading: false,
|
||||
minSearchStringLength: 1,
|
||||
maxAutocompleteResults: 20,
|
||||
suggestions: [],
|
||||
preExistingSharees: [...this.receivers],
|
||||
localSharees: this.receivers.map(userObject => userObject.user),
|
||||
localSharees: this.receivers.map(userObject => userObject.id),
|
||||
}
|
||||
},
|
||||
|
||||
@ -71,117 +60,35 @@ export default {
|
||||
return this.localSharees
|
||||
},
|
||||
set(v) {
|
||||
this.localSharees = v.map(userObject => userObject.user)
|
||||
this.localSharees = v.map(userObject => userObject.id)
|
||||
this.$emit('update', v)
|
||||
},
|
||||
},
|
||||
|
||||
isValidQuery() {
|
||||
return this.query?.trim() && this.query.length >= this.minSearchStringLength
|
||||
},
|
||||
|
||||
options() {
|
||||
if (this.isValidQuery) {
|
||||
return this.suggestions
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
noResultText() {
|
||||
if (this.loading) {
|
||||
return t('tables', 'Searching …')
|
||||
}
|
||||
return t('tables', 'No elements found.')
|
||||
},
|
||||
|
||||
userId() {
|
||||
return getCurrentUser().uid
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addShare(selectedItem) {
|
||||
if (selectedItem) {
|
||||
this.localValue = selectedItem
|
||||
} else {
|
||||
this.localValue = []
|
||||
this.localValue = selectedItem
|
||||
},
|
||||
|
||||
filterOutUnwantedItems(items) {
|
||||
// Filter out existing items
|
||||
items = items.filter((item) => !(item.isUser && this.localSharees.includes(item.id)))
|
||||
|
||||
// Filter out current user
|
||||
return items.filter((item) => !(item.isUser && item.id === this.currentUserId))
|
||||
},
|
||||
|
||||
formatResult(autocompleteResult) {
|
||||
return {
|
||||
id: autocompleteResult.id,
|
||||
displayName: autocompleteResult.label,
|
||||
icon: autocompleteResult.icon,
|
||||
isUser: autocompleteResult.source.startsWith('users'),
|
||||
key: autocompleteResult.source + '-' + autocompleteResult.id,
|
||||
}
|
||||
},
|
||||
|
||||
getShareTypes() {
|
||||
const types = []
|
||||
if (this.selectUsers) {
|
||||
types.push(this.SHARE_TYPES.SHARE_TYPE_USER)
|
||||
}
|
||||
if (this.selectGroups) {
|
||||
types.push(this.SHARE_TYPES.SHARE_TYPE_GROUP)
|
||||
}
|
||||
return types
|
||||
},
|
||||
|
||||
getShareTypeString() {
|
||||
if (this.selectUsers && !this.selectGroups) {
|
||||
return 'User'
|
||||
} else if (!this.selectUsers && this.selectGroups) {
|
||||
return 'Group'
|
||||
} else {
|
||||
return 'User or group'
|
||||
}
|
||||
},
|
||||
|
||||
getPlaceholder() {
|
||||
return t('tables', '{shareTypeString}...', { shareTypeString: this.getShareTypeString() })
|
||||
},
|
||||
|
||||
async asyncFind(query) {
|
||||
this.query = query.trim()
|
||||
if (this.isValidQuery) {
|
||||
this.loading = true
|
||||
await this.debounceGetSuggestions(query)
|
||||
}
|
||||
},
|
||||
|
||||
async getSuggestions(search) {
|
||||
this.loading = true
|
||||
const shareTypes = this.getShareTypes()
|
||||
let shareTypeQueryString = ''
|
||||
shareTypes.forEach(shareType => {
|
||||
shareTypeQueryString += `&shareTypes[]=${shareType}`
|
||||
})
|
||||
const url = generateOcsUrl(`core/autocomplete/get?search=${search}${shareTypeQueryString}&limit=${this.maxAutocompleteResults}`)
|
||||
|
||||
try {
|
||||
const res = await axios.get(url)
|
||||
const rawSuggestions = res.data.ocs.data.map(autocompleteResult => {
|
||||
return {
|
||||
user: autocompleteResult.id,
|
||||
displayName: autocompleteResult.label,
|
||||
icon: autocompleteResult.icon,
|
||||
isUser: autocompleteResult.source.startsWith('users'),
|
||||
key: autocompleteResult.source + '-' + autocompleteResult.id,
|
||||
}
|
||||
})
|
||||
|
||||
this.suggestions = this.filterOutCurrentUser(rawSuggestions)
|
||||
this.suggestions = this.filterOutSelectedUsers(this.suggestions)
|
||||
this.loading = false
|
||||
} catch (err) {
|
||||
console.debug(err)
|
||||
showError(t('tables', 'Failed to fetch {shareTypeString}', { shareTypeString: this.getShareTypeString().toLowerCase() }))
|
||||
}
|
||||
},
|
||||
|
||||
debounceGetSuggestions: debounce(function(...args) {
|
||||
this.getSuggestions(...args)
|
||||
}, 300),
|
||||
|
||||
filterOutCurrentUser(list) {
|
||||
return list.filter((item) => !(item.isUser && item.user === getCurrentUser().uid))
|
||||
},
|
||||
filterOutSelectedUsers(list) {
|
||||
return list.filter((item) => !(item.isUser && this.localSharees.includes(item.user)))
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
80
src/shared/components/ncUserPicker/NcUserPicker.vue
Normal file
80
src/shared/components/ncUserPicker/NcUserPicker.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="row space-B">
|
||||
<NcSelect v-model="value" style="width: 100%;" :loading="loading" :options="options"
|
||||
:placeholder="getPlaceholder()" :searchable="true" :get-option-key="(option) => option.key"
|
||||
label="displayName" :aria-label-combobox="getPlaceholder()" :user-select="true" @search="asyncFind"
|
||||
@input="addTransfer">
|
||||
<template #noResult>
|
||||
{{ noResultText }}
|
||||
</template>
|
||||
</NcSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcSelect } from '@nextcloud/vue'
|
||||
import formatting from '../../mixins/formatting.js'
|
||||
import '@nextcloud/dialogs/style.css'
|
||||
import searchUserGroup from '../../mixins/searchUserGroup.js'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
NcSelect,
|
||||
},
|
||||
|
||||
mixins: [formatting, searchUserGroup],
|
||||
|
||||
props: {
|
||||
selectedUserId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectUsers: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
selectGroups: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.selectedUserId
|
||||
},
|
||||
set(v) {
|
||||
this.$emit('update:selectedUserId', v?.id)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addTransfer(selectedItem) {
|
||||
this.localValue = selectedItem
|
||||
},
|
||||
|
||||
filterOutUnwantedItems(items) {
|
||||
return items.filter((item) => !(item.isUser && item.id === this.currentUserId))
|
||||
},
|
||||
|
||||
formatResult(autocompleteResult) {
|
||||
return {
|
||||
id: autocompleteResult.id,
|
||||
displayName: autocompleteResult.label,
|
||||
icon: autocompleteResult.icon,
|
||||
isUser: autocompleteResult.source.startsWith('users'),
|
||||
key: autocompleteResult.source + '-' + autocompleteResult.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,82 +1,33 @@
|
||||
<template>
|
||||
<div class="row space-B">
|
||||
<NcSelect id="transfer-ownership-select" v-model="value" style="width: 100%;" :loading="loading" :options="options"
|
||||
:placeholder="getPlaceholder()"
|
||||
:searchable="true" :get-option-key="(option) => option.key"
|
||||
label="displayName"
|
||||
:aria-label-combobox="getPlaceholder()" :user-select="true"
|
||||
@search="asyncFind" @input="addTransfer">
|
||||
<template #noResult>
|
||||
{{ noResultText }}
|
||||
</template>
|
||||
</NcSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import { NcSelect } from '@nextcloud/vue'
|
||||
import formatting from '../../../shared/mixins/formatting.js'
|
||||
import ShareTypes from '../../mixins/shareTypesMixin.js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import '@nextcloud/dialogs/style.css'
|
||||
import ShareTypes from './shareTypesMixin.js'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
NcSelect,
|
||||
},
|
||||
|
||||
mixins: [ShareTypes, formatting],
|
||||
|
||||
props: {
|
||||
newOwnerUserId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectUsers: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
selectGroups: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [ShareTypes],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
value: '',
|
||||
loading: false,
|
||||
minSearchStringLength: 1,
|
||||
maxAutocompleteResults: 20,
|
||||
suggestions: [],
|
||||
recommendations: [],
|
||||
currentUserId: getCurrentUser().uid,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.newOwnerUserId
|
||||
},
|
||||
set(v) {
|
||||
this.$emit('update:newOwnerUserId', v)
|
||||
},
|
||||
},
|
||||
|
||||
isValidQuery() {
|
||||
return this.query?.trim() && this.query.length >= this.minSearchStringLength
|
||||
},
|
||||
|
||||
options() {
|
||||
if (this.isValidQuery) {
|
||||
return this.suggestions
|
||||
}
|
||||
return []
|
||||
return this.recommendations
|
||||
},
|
||||
|
||||
noResultText() {
|
||||
@ -86,20 +37,8 @@ export default {
|
||||
return t('tables', 'No elements found.')
|
||||
},
|
||||
|
||||
userId() {
|
||||
return getCurrentUser().uid
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
addTransfer(selectedItem) {
|
||||
if (selectedItem) {
|
||||
this.localValue = selectedItem.user
|
||||
} else {
|
||||
this.localValue = ''
|
||||
}
|
||||
},
|
||||
|
||||
getShareTypes() {
|
||||
const types = []
|
||||
if (this.selectUsers) {
|
||||
@ -110,7 +49,6 @@ export default {
|
||||
}
|
||||
return types
|
||||
},
|
||||
|
||||
getShareTypeString() {
|
||||
if (this.selectUsers && !this.selectGroups) {
|
||||
return 'User'
|
||||
@ -120,11 +58,9 @@ export default {
|
||||
return 'User or group'
|
||||
}
|
||||
},
|
||||
|
||||
getPlaceholder() {
|
||||
return t('tables', '{shareTypeString}...', { shareTypeString: this.getShareTypeString() })
|
||||
},
|
||||
|
||||
async asyncFind(query) {
|
||||
this.query = query.trim()
|
||||
if (this.isValidQuery) {
|
||||
@ -132,7 +68,6 @@ export default {
|
||||
await this.debounceGetSuggestions(query)
|
||||
}
|
||||
},
|
||||
|
||||
async getSuggestions(search) {
|
||||
this.loading = true
|
||||
const shareTypes = this.getShareTypes()
|
||||
@ -144,17 +79,10 @@ export default {
|
||||
|
||||
try {
|
||||
const res = await axios.get(url)
|
||||
const rawSuggestions = res.data.ocs.data.map(autocompleteResult => {
|
||||
return {
|
||||
user: autocompleteResult.id,
|
||||
displayName: autocompleteResult.label,
|
||||
icon: autocompleteResult.icon,
|
||||
isUser: autocompleteResult.source.startsWith('users'),
|
||||
key: autocompleteResult.source + '-' + autocompleteResult.id,
|
||||
}
|
||||
const rawSuggestions = res.data.ocs.data.map(result => {
|
||||
return this.formatResult(result)
|
||||
})
|
||||
|
||||
this.suggestions = this.filterOutCurrentUser(rawSuggestions)
|
||||
this.suggestions = this.filterOutUnwantedItems(rawSuggestions)
|
||||
this.loading = false
|
||||
} catch (err) {
|
||||
console.debug(err)
|
||||
@ -166,17 +94,5 @@ export default {
|
||||
this.getSuggestions(...args)
|
||||
}, 300),
|
||||
|
||||
filterOutCurrentUser(list) {
|
||||
return list.filter((item) => !(item.isUser && item.user === getCurrentUser().uid))
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.multiselect {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
@ -348,7 +348,7 @@ export default new Vuex.Store({
|
||||
try {
|
||||
for (const receiver of receivers) {
|
||||
share.receiverType = receiver.isUser ? 'user' : 'group'
|
||||
share.receiver = receiver.user
|
||||
share.receiver = receiver.id
|
||||
// Avoid duplicate shares by checking if share exists first
|
||||
const existingShare = previousReceivers.find((p) => p.receiver === share.receiver && p.receiver_type === share.receiverType)
|
||||
if (!existingShare) {
|
||||
@ -363,7 +363,7 @@ export default new Vuex.Store({
|
||||
for (const previousReceiver of previousReceivers) {
|
||||
const currentShare = receivers.find((r) => {
|
||||
const receiverType = r.isUser ? 'user' : 'group'
|
||||
return r.user === previousReceiver.receiver && receiverType === previousReceiver.receiver_type
|
||||
return r.id === previousReceiver.receiver && receiverType === previousReceiver.receiver_type
|
||||
})
|
||||
if (!currentShare) {
|
||||
await axios.delete(generateUrl('/apps/tables/share/' + previousReceiver.share_id))
|
||||
|
Reference in New Issue
Block a user