Skip to content

Commit

Permalink
Merge pull request #4120 from nextcloud/feat/improve-ncsettingsselect…
Browse files Browse the repository at this point in the history
…group

Improve `NcSettingsSelectGroup` - Convert to `NcSelect`
  • Loading branch information
nickvergessen committed Jun 22, 2023
2 parents 28f7154 + 227134b commit 5a5f2b2
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 36 deletions.
178 changes: 143 additions & 35 deletions src/components/NcSettingsSelectGroup/NcSettingsSelectGroup.vue
Expand Up @@ -3,6 +3,7 @@
-
- @author Julius Härtl <jus@bitgrid.net>
- @author Greta Doci <gretadoci@gmail.com>
- @author Ferdinand Thiessen <opensource@fthiessen.de>
-
- @license GNU AGPL version 3 or any later version
-
Expand All @@ -21,55 +22,82 @@
-
-->

<docs>
```vue
<template>
<NcMultiselect :value="inputValue"
:options="groupsArray"
:options-limit="5"
:placeholder="label"
track-by="id"
label="displayname"
class="multiselect-vue"
:multiple="true"
:close-on-select="false"
:tag-width="60"
:disabled="disabled"
@input="update"
@search-change="findGroup">
<template #noResult>
<span>{{ t( 'No results') }}</span>
</template>
</NcMultiselect>
<section>
<NcSettingsSelectGroup v-model="groups" placeholder="Select user groups" label="The hidden label" />
<NcSettingsSelectGroup v-model="otherGroups" :disabled="true" label="Also a fallback for the placeholder" />
<div>You have selected: <code>{{ groups }}</code> and <code>{{ otherGroups }}</code></div>
</section>
</template>
<script>
export default {
data() {
return {
groups: [],
otherGroups: ['admin']
}
}
}
</script>
<style scoped>
section * {
padding: 6px 0px;
}
</style>
```
</docs>

<template>
<div>
<label v-if="label" :for="id" class="hidden-visually">{{ label }}</label>
<NcSelect :value="inputValue"
:options="groupsArray"
:placeholder="placeholder || label"
:filter-by="filterGroups"
:input-id="id"
:limit="5"
label="displayname"
:multiple="true"
:close-on-select="false"
:disabled="disabled"
@input="update"
@search="onSearch" />
</div>
</template>

<script>
import NcMultiselect from '../../components/NcMultiselect/index.js'
import NcSelect from '../../components/NcSelect/index.js'
import { t } from '../../l10n.js'
import l10n from '../../mixins/l10n.js'
import GenRandomId from '../../utils/GenRandomId.js'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { generateOcsUrl } from '@nextcloud/router'
import { debounce } from 'debounce'
export default {
name: 'NcSettingsSelectGroup',
components: {
NcMultiselect,
NcSelect,
},
mixins: [l10n],
props: {
/**
* label of the select group element
* The text of the label element of the select group input
*/
label: {
type: String,
required: true,
},
/**
* hint of the select group input
* Placeholder for the input element
* For backwards compatibility it falls back to the `label` value
*/
hint: {
placeholder: {
type: String,
default: '',
},
Expand All @@ -85,6 +113,7 @@ export default {
/**
* value of the select group input
* A list of group IDs can be provided
*/
value: {
type: Array,
Expand All @@ -105,16 +134,26 @@ export default {
],
data() {
return {
/** Temporary store to cache groups */
groups: {},
randId: GenRandomId(),
}
},
computed: {
inputValue() {
return this.getValueObject()
/**
* Validate input value and only return valid strings (group IDs)
*
* @return {string[]}
*/
filteredValue() {
return this.value.filter((group) => group !== '' && typeof group === 'string')
},
getValueObject() {
return this.value.filter((group) => group !== '' && typeof group !== 'undefined').map(
/**
* value property converted to an array of group objects used as input for the NcSelect
*/
inputValue() {
return this.filteredValue.map(
(id) => {
if (typeof this.groups[id] === 'undefined') {
return {
Expand All @@ -126,33 +165,102 @@ export default {
}
)
},
/**
* Convert groups object to array of groups required for NcSelect.options
* Filter out currently selected values
*
* @return {object[]}
*/
groupsArray() {
return Object.values(this.groups)
return Object.values(this.groups).filter(g => !this.value.includes(g.id))
},
},
watch: {
/**
* If the value is changed, check that all groups are loaded so we show the correct display name
*/
value: {
handler() {
const loadedGroupIds = Object.keys(this.groups)
const missing = this.filteredValue.filter(group => !loadedGroupIds.includes(group))
missing.forEach((groupId) => {
this.loadGroup(groupId)
})
},
// Run the watch handler also when the component is initially mounted
immediate: true,
},
},
/**
* Load groups matching the empty query to reduce API calls
*/
async mounted() {
// version scoped to prevent issues with different library versions
const storageName = `${appName}:${appVersion}/initialGroups`
let savedGroups = window.sessionStorage.getItem(storageName)
if (savedGroups) {
savedGroups = Object.fromEntries(JSON.parse(savedGroups).map(group => [group.id, group]))
this.groups = { ...this.groups, ...savedGroups }
} else {
await this.loadGroup('')
window.sessionStorage.setItem(storageName, JSON.stringify(Object.values(this.groups)))
}
},
methods: {
update() {
this.$emit('input', this.inputValue.map((element) => element.id))
/**
* Called when a new group is selected or previous group is deselected to emit the update event
*
* @param {object[]} updatedValue Array of selected groups
*/
update(updatedValue) {
const value = updatedValue.map((element) => element.id)
/** Emitted when the groups selection changes<br />**Payload:** `value` (`Array`) - *Ids of selected groups */
this.$emit('input', value)
},
async findGroup(query) {
/**
* Use provisioning API to search for given group and save it in the groups object
*
* @param {string} query The query like parts of the id oder display name
* @return {boolean}
*/
async loadGroup(query) {
try {
query = typeof query === 'string' ? encodeURI(query) : ''
const response = await axios.get(generateOcsUrl(`cloud/groups/details?search=${query}&limit=10`, 2))
if (Object.keys(response.data.ocs.data.groups).length > 0) {
response.data.ocs.data.groups.forEach((element) => {
if (typeof this.groups[element.id] === 'undefined') {
this.$set(this.groups, element.id, element)
}
})
const newGroups = Object.fromEntries(response.data.ocs.data.groups.map((element) => [element.id, element]))
this.groups = { ...this.groups, ...newGroups }
return true
}
} catch (error) {
/** Emitted if groups could not be queried.<br />**Payload:** `error` (`object`) - The Axios error */
this.$emit('error', error)
showError(t('Unable to search the group'))
}
return false
},
/**
* Custom filter function for `NcSelect` to filter by ID *and* display name
*
* @param {object} option One of the groups
* @param {string} label The label property of the group
* @param {string} search The current search string
*/
filterGroups(option, label, search) {
return `${label || ''} ${option.id}`.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) > -1
},
/**
* Debounce the group search (reduce API calls)
*/
onSearch: debounce(function(query) {
this.loadGroup(query)
}, 200),
},
}
</script>
35 changes: 34 additions & 1 deletion styleguide/global.requires.js
Expand Up @@ -3,7 +3,40 @@ import 'core-js/stable'
/* eslint-disable-next-line */
import 'regenerator-runtime/runtime'
import Vue from 'vue'
import VTooltip from './../src/directives/Tooltip'
import VTooltip from './../src/directives/Tooltip/index.js'

import axios from '@nextcloud/axios'

const USER_GROUPS = [
{ id: 'admin', displayname: 'The administrators' },
{ id: 'accounting', displayname: 'Accounting team' },
{ id: 'developer', displayname: 'Engineering team' },
{ id: 'support', displayname: 'Support crew' },
{ id: 'users', displayname: 'users' },
]

/**
* Mock some requests for docs
*
* @param {object} error Axios error
*/
function mockRequests(error) {
const { request } = error
let data = null

// Mock requesting groups
const requestGroups = request.responseURL.match(/cloud\/groups\/details\?search=([^&]*)&limit=\d+$/)
if (requestGroups) {
data = { groups: USER_GROUPS.filter(e => !requestGroups[1] || e.displayname.startsWith(requestGroups[1]) || e.id.startsWith(requestGroups[1])) }
}

if (data) {
return Promise.resolve({ data: { ocs: { data } } })
}
return Promise.reject(error)
}

axios.interceptors.response.use((r) => r, e => mockRequests(e))

/**
* From server util.js
Expand Down

0 comments on commit 5a5f2b2

Please sign in to comment.