55
66<template >
77 <section class =" member-list" >
8- <NcEmptyContent v-if =" loading " class="empty-content" :name =" t (' contacts' , ' Loading members list …' )" >
8+ <NcEmptyContent v-if =" loading || loadingList " class="empty-content" :name =" t (' contacts' , ' Loading members list …' )" >
99 <template #icon >
10- <IconLoading :size =" 20 " />
10+ <NcLoadingIcon :size =" 20 " />
1111 </template >
1212 </NcEmptyContent >
1313
2020 </template >
2121 </NcEmptyContent >
2222
23- <NcEmptyContent
24- v-else-if =" ! hasMembers "
25- class="empty-content"
26- :name =" t (' contacts' , ' You currently have no access to the member list' )" >
27- <template #icon >
28- <IconContact :size =" 20 " />
23+ <template v-else >
24+ <div style =" display : flex ; margin-bottom : 2rem ;" >
25+ <NcTextField
26+ v-model =" searchQuery "
27+ :label =" t (' contacts' , ' Search among current members' )"
28+ trailing-button-icon="close"
29+ :show-trailing-button =" searchQuery !== ' ' "
30+ @trailing-button-click =" clearSearchField " >
31+ <IconSearch :size =" 20 " />
32+ </NcTextField >
33+
34+ <NcSelect
35+ v-model =" searchRole "
36+ :options =" roles "
37+ :multiple =" false "
38+ style =" margin-top : 6px ;min-width : 200px ;margin-left : 1rem ;" />
39+ </div >
40+
41+ <NcEmptyContent
42+ v-if =" ! hasMembers "
43+ class="empty-content"
44+ :name =" hasActiveFilters ? t (' contacts' , ' No members found matching your search' ) : t (' contacts' , ' You currently have no access to the member list' )" >
45+ <template #icon >
46+ <IconContact :size =" 20 " />
47+ </template >
48+ </NcEmptyContent >
49+
50+ <template v-else >
51+ <VList
52+ v-slot =" { item } "
53+ class="member-list__virtual"
54+ :style =" virtualListStyle "
55+ :data =" flatList " >
56+ <MemberGridItem
57+ :key =" ` member-grid-item-${item .id } ` "
58+ :member =" item "
59+ :is-team =" ! item .isUser " />
60+ </VList >
61+
62+ <div v-if =" isMembersLisTooLarge" class =" member-list__too-large" >
63+ {{ membershipTooLargeMessage }}
64+ </div >
2965 </template >
30- </NcEmptyContent >
31-
32- <VList
33- v-else
34- v-slot =" { item } "
35- class="member-list__virtual"
36- :style =" virtualListStyle "
37- :data =" flatList " >
38- <MemberGridItem
39- :key =" ` member-grid-item-${item .id } ` "
40- :member =" item "
41- :is-team =" ! item .isUser " />
42- </VList >
66+ </template >
4367
4468 <!-- member picker -->
4569 <EntityPicker
6286import { showError , showWarning } from ' @nextcloud/dialogs'
6387import { subscribe } from ' @nextcloud/event-bus'
6488import { t } from ' @nextcloud/l10n'
65- import { NcEmptyContent } from ' @nextcloud/vue'
89+ import { NcEmptyContent , NcLoadingIcon , NcSelect , NcTextField } from ' @nextcloud/vue'
90+ import { refDebounced } from ' @vueuse/core'
6691import { VList } from ' virtua/vue'
67- import { defineComponent } from ' vue'
92+ import { defineComponent , ref } from ' vue'
6893import IconContact from ' vue-material-design-icons/AccountMultipleOutline.vue'
94+ import IconSearch from ' vue-material-design-icons/Magnify.vue'
6995import EntityPicker from ' ../EntityPicker/EntityPicker.vue'
7096import MemberGridItem from ' ./MemberGridItem.vue'
7197import IsMobileMixin from ' ../../mixins/IsMobileMixin.ts'
7298import RouterMixin from ' ../../mixins/RouterMixin.js'
73- import { CIRCLES_MEMBER_GROUPING , SHARES_TYPES_MEMBER_MAP } from ' ../../models/constants.ts'
99+ import {
100+ CIRCLES_MEMBER_GROUPING ,
101+ CIRCLES_MEMBER_LEVELS ,
102+ MAX_MEMBERS_TO_RENDER ,
103+ MemberLevels ,
104+ SHARES_TYPES_MEMBER_MAP ,
105+ } from ' ../../models/constants.ts'
74106import { getRecommendations , getSuggestions } from ' ../../services/collaborationAutocompletion.js'
75107
76108export default defineComponent ({
77109 name: ' MemberList' ,
78110
79111 components: {
112+ IconSearch ,
113+ NcTextField ,
114+ NcSelect ,
80115 EntityPicker ,
81116 IconContact ,
82117 MemberGridItem ,
83118 NcEmptyContent ,
84119 VList ,
120+ NcLoadingIcon ,
85121 },
86122
87123 mixins: [IsMobileMixin , RouterMixin ],
@@ -98,8 +134,35 @@ export default defineComponent({
98134 },
99135 },
100136
137+ setup() {
138+ const searchQuery = ref (' ' )
139+ const clearSearchField = () => {
140+ searchQuery .value = ' '
141+ }
142+ const searchQueryDebounced = refDebounced (searchQuery , 500 )
143+
144+ const searchRole = ref (null )
145+ const roles = Object .entries (CIRCLES_MEMBER_LEVELS ).map (([id , label ]) => ({
146+ id: Number (id ),
147+ label ,
148+ }))
149+ roles .unshift ({
150+ id: Number (MemberLevels .NONE ),
151+ label: t (' contacts' , ' Pending' ),
152+ })
153+
154+ return {
155+ searchQuery ,
156+ searchQueryDebounced ,
157+ clearSearchField ,
158+ searchRole ,
159+ roles ,
160+ }
161+ },
162+
101163 data() {
102164 return {
165+ loadingList: false ,
103166 pickerLoading: false ,
104167 showPicker: false ,
105168 showPickerIntro: true ,
@@ -118,12 +181,30 @@ export default defineComponent({
118181 /**
119182 * Return the current circle
120183 *
121- * @return {object }
184+ * @return {Circle }
122185 */
123186 circle() {
124187 return this .$store .getters .getCircle (this .selectedCircle )
125188 },
126189
190+ members() {
191+ return Object .values (this .$store .getters .getCircle (this .circle .id )?.members || [])
192+ },
193+
194+ membershipTooLargeMessage() {
195+ if (this .searchQueryDebounced || this .searchRole ?.id ) {
196+ const searchQuery = this .searchQueryDebounced || ' -'
197+ const searchRole = this .searchRole ?.label || ' any'
198+ return ` Search results (query: ${searchQuery }, role: ${searchRole }) contains too many entries. `
199+ }
200+
201+ return ' Users list too large'
202+ },
203+
204+ isMembersLisTooLarge() {
205+ return this .flatList .length > MAX_MEMBERS_TO_RENDER
206+ },
207+
127208 // Decode HTML entities in the circle display name so apostrophes (') and other
128209 // HTML-encoded chars (e.g. ') are shown correctly in the picker labels.
129210 decodedTeamName(): string {
@@ -157,6 +238,10 @@ export default defineComponent({
157238 return this .flatList .length > 0
158239 },
159240
241+ hasActiveFilters() {
242+ return this .searchQuery !== ' ' || this .searchRole !== null
243+ },
244+
160245 virtualListStyle() {
161246 const gridBaseline = parseInt (getComputedStyle (document .documentElement ).getPropertyValue (' --default-grid-baseline' )) || 4
162247 const headerHeight = parseInt (getComputedStyle (document .documentElement ).getPropertyValue (' --header-height' )) || 50
@@ -168,6 +253,24 @@ export default defineComponent({
168253 },
169254 },
170255
256+ watch: {
257+ searchQueryDebounced(value ) {
258+ this .fetchCircleMembers ()
259+ },
260+
261+ searchRole(value ) {
262+ this .fetchCircleMembers ()
263+ },
264+
265+ ' circle.id' : {
266+ handler() {
267+ this .fetchCircleMembers ()
268+ },
269+
270+ immediate: true ,
271+ },
272+ },
273+
171274 mounted() {
172275 subscribe (' contacts:circles:append' , this .onShowPicker )
173276 subscribe (' guests:user:created' , this .onGuestCreated )
@@ -179,6 +282,8 @@ export default defineComponent({
179282 },
180283
181284 methods: {
285+ t ,
286+
182287 /**
183288 * Measure the circle details header height from the DOM
184289 * and keep it updated via ResizeObserver.
@@ -296,6 +401,26 @@ export default defineComponent({
296401 const results = await getSuggestions (guest .username , this .circle )
297402 this .$refs .entityPicker .onClick (results [0 ])
298403 },
404+
405+ async fetchCircleMembers() {
406+ if (! this .circle ?.canManageMembers ) {
407+ return
408+ }
409+
410+ this .loadingList = true
411+ const payload = { circleId: this .circle .id , search: this .searchQuery || null , role: this .searchRole ?.id }
412+ this .logger .debug (' Fetching members for' , payload )
413+
414+ try {
415+ await this .$store .dispatch (' getCircleMembers' , payload )
416+ console .log (' debug: getCircleMembers' , this .list )
417+ } catch (error ) {
418+ console .error (error )
419+ showError (t (' contacts' , ' There was an error fetching the member list' ))
420+ } finally {
421+ this .loadingList = false
422+ }
423+ },
299424 },
300425})
301426 </script >
@@ -312,4 +437,10 @@ export default defineComponent({
312437.empty-content {
313438 height : 100% ;
314439}
440+
441+ .member-list__too-large {
442+ padding : var (--default-grid-baseline ) 0 ;
443+ text-align : center ;
444+ color : var (--color-text-maxcontrast );
445+ }
315446 </style >
0 commit comments