Skip to content

Commit 4816864

Browse files
committed
feat: optimize members list
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 0e19648 commit 4816864

8 files changed

Lines changed: 189 additions & 43 deletions

File tree

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/AppContent/CircleContent.vue

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,10 @@ export default {
8282
return Object.values(this.circle?.members || [])
8383
},
8484
85-
/**
86-
* Is the current circle empty
87-
*
88-
* @return {boolean}
89-
*/
90-
isEmptyCircle() {
91-
return this.members.length === 0
92-
},
93-
9485
...mapStores(useUserGroupStore),
9586
},
9687
9788
watch: {
98-
circle(newCircle) {
99-
if (newCircle?.id) {
100-
this.fetchCircleMembers(newCircle.id)
101-
}
102-
},
103-
10489
userGroup(newUserGroup) {
10590
if (newUserGroup?.id) {
10691
this.fetchUserGroupMembers(newUserGroup.id)
@@ -109,29 +94,12 @@ export default {
10994
},
11095
11196
beforeMount() {
112-
if (this.circle?.id) {
113-
this.fetchCircleMembers(this.circle.id)
114-
}
115-
11697
if (this.userGroup?.id) {
11798
this.fetchUserGroupMembers(this.userGroup.id)
11899
}
119100
},
120101
121102
methods: {
122-
async fetchCircleMembers(circleId) {
123-
this.loadingList = true
124-
this.logger.debug('Fetching members for', { circleId })
125-
126-
try {
127-
await this.$store.dispatch('getCircleMembers', circleId)
128-
} catch (error) {
129-
console.error(error)
130-
showError(t('contacts', 'There was an error fetching the member list'))
131-
} finally {
132-
this.loadingList = false
133-
}
134-
},
135103
136104
async fetchUserGroupMembers(userGroupId) {
137105
this.loadingList = true

src/components/MemberList/MemberList.vue

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<section class="member-list">
88
<NcEmptyContent v-if="loading" 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

@@ -62,26 +62,43 @@
6262
import { showError, showWarning } from '@nextcloud/dialogs'
6363
import { subscribe } from '@nextcloud/event-bus'
6464
import { t } from '@nextcloud/l10n'
65-
import { NcEmptyContent } from '@nextcloud/vue'
65+
import { NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
66+
import { refDebounced } from '@vueuse/core'
6667
import { VList } from 'virtua/vue'
67-
import { defineComponent } from 'vue'
68+
import { defineComponent, ref } from 'vue'
6869
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
70+
import IconSearch from 'vue-material-design-icons/Magnify.vue'
71+
import IconAdd from 'vue-material-design-icons/Plus.vue'
6972
import EntityPicker from '../EntityPicker/EntityPicker.vue'
7073
import MemberGridItem from './MemberGridItem.vue'
7174
import IsMobileMixin from '../../mixins/IsMobileMixin.ts'
7275
import RouterMixin from '../../mixins/RouterMixin.js'
73-
import { CIRCLES_MEMBER_GROUPING, SHARES_TYPES_MEMBER_MAP } from '../../models/constants.ts'
76+
import {
77+
CIRCLES_MEMBER_GROUPING,
78+
CIRCLES_MEMBER_LEVELS,
79+
MAX_MEMBERS_TO_RENDER,
80+
MemberLevels,
81+
SHARES_TYPES_MEMBER_MAP,
82+
} from '../../models/constants.ts'
7483
import { getRecommendations, getSuggestions } from '../../services/collaborationAutocompletion.js'
7584
7685
export default defineComponent({
7786
name: 'MemberList',
7887
7988
components: {
89+
IconSearch,
90+
NcTextField,
91+
NcSelect,
92+
IconAdd,
93+
MemberGridItem,
94+
NcButton,
8095
EntityPicker,
8196
IconContact,
8297
MemberGridItem,
8398
NcEmptyContent,
8499
VList,
100+
NcLoadingIcon,
101+
ContentHeading,
85102
},
86103
87104
mixins: [IsMobileMixin, RouterMixin],
@@ -98,8 +115,35 @@ export default defineComponent({
98115
},
99116
},
100117
118+
setup() {
119+
const searchQuery = ref('')
120+
const clearSearchField = () => {
121+
searchQuery.value = ''
122+
}
123+
const searchQueryDebounced = refDebounced(searchQuery, 500)
124+
125+
const searchRole = ref(null)
126+
const roles = Object.entries(CIRCLES_MEMBER_LEVELS).map(([id, label]) => ({
127+
id: Number(id),
128+
label,
129+
}))
130+
roles.unshift({
131+
id: Number(MemberLevels.NONE),
132+
label: t('contacts', 'Pending'),
133+
})
134+
135+
return {
136+
searchQuery,
137+
searchQueryDebounced,
138+
clearSearchField,
139+
searchRole,
140+
roles,
141+
}
142+
},
143+
101144
data() {
102145
return {
146+
loadingList: false,
103147
pickerLoading: false,
104148
showPicker: false,
105149
showPickerIntro: true,
@@ -118,12 +162,30 @@ export default defineComponent({
118162
/**
119163
* Return the current circle
120164
*
121-
* @return {object}
165+
* @return {Circle}
122166
*/
123167
circle() {
124168
return this.$store.getters.getCircle(this.selectedCircle)
125169
},
126170
171+
members() {
172+
return Object.values(this.$store.getters.getCircle(this.circle.id)?.members || [])
173+
},
174+
175+
membershipTooLargeMessage() {
176+
if (this.searchQueryDebounced || this.searchRole?.id) {
177+
const searchQuery = this.searchQueryDebounced || '-'
178+
const searchRole = this.searchRole?.label || 'any'
179+
return `Search results (query: ${searchQuery}, role: ${searchRole}) contains too many entries.`
180+
}
181+
182+
return 'Users list too large'
183+
},
184+
185+
isMembersLisTooLarge() {
186+
return this.flatList.length > MAX_MEMBERS_TO_RENDER
187+
},
188+
127189
// Decode HTML entities in the circle display name so apostrophes (') and other
128190
// HTML-encoded chars (e.g. &#39;) are shown correctly in the picker labels.
129191
decodedTeamName(): string {
@@ -168,6 +230,24 @@ export default defineComponent({
168230
},
169231
},
170232
233+
watch: {
234+
searchQueryDebounced(value) {
235+
this.fetchCircleMembers()
236+
},
237+
238+
searchRole(value) {
239+
this.fetchCircleMembers()
240+
},
241+
242+
'circle.id': {
243+
handler() {
244+
this.fetchCircleMembers()
245+
},
246+
247+
immediate: true,
248+
},
249+
},
250+
171251
mounted() {
172252
subscribe('contacts:circles:append', this.onShowPicker)
173253
subscribe('guests:user:created', this.onGuestCreated)
@@ -179,6 +259,8 @@ export default defineComponent({
179259
},
180260
181261
methods: {
262+
t,
263+
182264
/**
183265
* Measure the circle details header height from the DOM
184266
* and keep it updated via ResizeObserver.
@@ -296,6 +378,26 @@ export default defineComponent({
296378
const results = await getSuggestions(guest.username, this.circle)
297379
this.$refs.entityPicker.onClick(results[0])
298380
},
381+
382+
async fetchCircleMembers() {
383+
if (!this.circle?.canManageMembers) {
384+
return
385+
}
386+
387+
this.loadingList = true
388+
const payload = { circleId: this.circle.id, searchQuery: this.searchQuery || null, role: this.searchRole?.id }
389+
this.logger.debug('Fetching members for', payload)
390+
391+
try {
392+
await this.$store.dispatch('getCircleMembers', payload)
393+
console.log('debug: getCircleMembers', this.list)
394+
} catch (error) {
395+
console.error(error)
396+
showError(t('contacts', 'There was an error fetching the member list'))
397+
} finally {
398+
this.loadingList = false
399+
}
400+
},
299401
},
300402
})
301403
</script>

src/components/MemberList/MemberListGroup.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<script setup lang="ts">
77
import type Member from '../../models/member.ts'
88
9+
import { t } from '@nextcloud/l10n'
10+
import { NcEmptyContent } from '@nextcloud/vue'
11+
import IconSearch from 'vue-material-design-icons/Magnify.vue'
912
import MemberListItem from './MemberListItem.vue'
1013
1114
defineProps<{
@@ -22,6 +25,17 @@ defineProps<{
2225
class="member-list-group__heading">
2326
{{ label }}
2427
</h4>
28+
29+
<template v-if="!members.length">
30+
<div style="margin-top: 2rem;">
31+
<NcEmptyContent :name="t('contacts', 'No results found')">
32+
<template #icon>
33+
<IconSearch :size="20" />
34+
</template>
35+
</NcEmptyContent>
36+
</div>
37+
</template>
38+
2539
<ul :aria-labelledby="`member-list-group-${type}`" class="member-list-group__list">
2640
<MemberListItem
2741
v-for="member in members"
@@ -32,6 +46,13 @@ defineProps<{
3246
</template>
3347

3448
<style scoped lang="scss">
49+
#member-list-group-1 {
50+
margin-top: 0;
51+
padding-top: 0;
52+
}
53+
.member-list-group__list-extended {
54+
max-height: 200px;
55+
}
3556
.member-list-group {
3657
&__heading {
3758
display: flex;

src/components/MemberList/MemberListItem.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ export default {
336336
// If we changed an owner, let's refresh the whole dataset to update all ownership & memberships
337337
if (level === MemberLevels.OWNER) {
338338
await this.$store.dispatch('getCircle', this.circle.id)
339-
await this.$store.dispatch('getCircleMembers', this.circle.id)
339+
await this.$store.dispatch('getCircleMembers', { circleId: this.circle.id })
340340
return
341341
}
342342

src/models/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,5 @@ export enum MemberStatus {
215215
MEMBER = 'Member',
216216
REQUESTING = 'Requesting',
217217
}
218+
// fixme: increase to 100+
219+
export const MAX_MEMBERS_TO_RENDER = 4

src/services/circles.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,22 @@ export async function leaveCircle(circleId: string) {
115115
* Get the circle members without the members
116116
*
117117
* @param circleId the circle id
118+
* @param search the search query
119+
* @param role the role
120+
* @param limit the limit
118121
* @return
119122
*/
120-
export async function getCircleMembers(circleId: string) {
121-
const response = await axios.get(generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId }))
123+
export async function getCircleMembers(circleId: string, search?: string, role?: string, limit: number = 100) {
124+
const response = await axios.get(
125+
generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId }),
126+
{
127+
params: {
128+
search,
129+
role,
130+
limit,
131+
},
132+
},
133+
)
122134
return response.data.ocs.data
123135
}
124136

0 commit comments

Comments
 (0)