Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve NcSettingsSelectGroup - Convert to NcSelect #4120

Merged
merged 4 commits into from Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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