Skip to content

Commit 2ca5cf7

Browse files
committed
feat: add virtual scroll to member list
Signed-off-by: ailkiv <[email protected]>
1 parent 0af386b commit 2ca5cf7

File tree

3 files changed

+95
-20
lines changed

3 files changed

+95
-20
lines changed

src/components/CircleDetails.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
{{ t('contacts', 'Add') }}
155155
</Button>
156156
</div>
157-
<MemberList ref="memberList" :list="members" />
157+
<MemberList ref="memberList" :list="members" :key="`member-list-${circle.id}`" />
158158
</div>
159159
</div>
160160
</section>

src/components/MemberList/MemberList.vue

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,40 @@
2727
</template>
2828
</NcEmptyContent>
2929

30-
<div v-else class="member-grid">
31-
<MemberGridItem v-for="member in flatList"
32-
:key="`member-grid-item-${member.id}`"
33-
:member="member"
34-
:is-team="!member.isUser" />
35-
</div>
30+
<template v-else>
31+
<NcTextField
32+
v-if="flatList.length > 20"
33+
v-model="searchQuery"
34+
:label="t('contacts', 'Search among current members')"
35+
trailing-button-icon="close"
36+
:show-trailing-button="searchQuery !== ''"
37+
@trailing-button-click="clearSearchField">
38+
<IconSearch :size="20" />
39+
</NcTextField>
40+
<RecycleScroller
41+
ref="scroller"
42+
class="member-scroller"
43+
:items="filteredList"
44+
:item-size="56"
45+
:grid-items="gridItems"
46+
:item-secondary-size="itemSecondarySize">
47+
<template #default="{ item }">
48+
<MemberGridItem
49+
:key="`member-grid-item-${item.id}`"
50+
:member="item"
51+
:is-team="!item.isUser" />
52+
</template>
53+
<template #empty v-if="!filteredList.length">
54+
<div style="margin-top: 2rem;">
55+
<NcEmptyContent :name="t('contacts', 'No results found')">
56+
<template #icon>
57+
<IconSearch :size="20" />
58+
</template>
59+
</NcEmptyContent>
60+
</div>
61+
</template>
62+
</RecycleScroller>
63+
</template>
3664

3765
<!-- member picker -->
3866
<EntityPicker v-if="showPicker"
@@ -53,11 +81,16 @@
5381
import {
5482
NcEmptyContent,
5583
isMobile,
84+
NcLoadingIcon as IconLoading,
85+
NcTextField
5686
} from '@nextcloud/vue'
87+
import { RecycleScroller } from 'vue-virtual-scroller'
88+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
5789
5890
import MemberGridItem from './MemberGridItem.vue'
5991
import EntityPicker from '../EntityPicker/EntityPicker.vue'
6092
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
93+
import IconSearch from 'vue-material-design-icons/Magnify.vue'
6194
6295
import RouterMixin from '../../mixins/RouterMixin.js'
6396
@@ -74,8 +107,12 @@ export default defineComponent({
74107
components: {
75108
EntityPicker,
76109
IconContact,
110+
IconLoading,
111+
IconSearch,
77112
MemberGridItem,
78113
NcEmptyContent,
114+
RecycleScroller,
115+
NcTextField,
79116
},
80117
81118
mixins: [isMobile, RouterMixin],
@@ -103,6 +140,8 @@ export default defineComponent({
103140
pickerData: [],
104141
pickerSelection: {},
105142
pickerTypes: CIRCLES_MEMBER_GROUPING,
143+
windowWidth: window.innerWidth,
144+
searchQuery: '',
106145
}
107146
},
108147
@@ -134,17 +173,59 @@ export default defineComponent({
134173
return [...teams, ...users]
135174
},
136175
176+
filteredList() {
177+
const query = this.searchQuery.toLowerCase()
178+
179+
return this.flatList.filter(member =>
180+
!this.searchQuery || member.displayName.toLowerCase().includes(query)
181+
);
182+
},
183+
137184
hasMembers() {
138185
return this.flatList.length > 0
139186
},
187+
188+
gridItems() {
189+
if (this.windowWidth < 768) {
190+
// undefined means that the grid will be rendered as a list
191+
return undefined
192+
}
193+
194+
return 2
195+
},
196+
197+
itemSecondarySize() {
198+
if (this.windowWidth < 768) {
199+
// undefined means that the grid will be rendered as a list
200+
return undefined
201+
}
202+
203+
// The maximum width of the member list is 500px,
204+
// so with two columns, each column is 242px wide (with scroll)
205+
return 242
206+
},
140207
},
141208
142209
mounted() {
143210
subscribe('contacts:circles:append', this.onShowPicker)
144211
subscribe('guests:user:created', this.onGuestCreated)
212+
213+
window.addEventListener('resize', this.onResize)
214+
},
215+
216+
beforeDestroy() {
217+
window.removeEventListener('resize', this.onResize)
145218
},
146219
147220
methods: {
221+
onResize() {
222+
this.windowWidth = window.innerWidth
223+
},
224+
225+
clearSearchField() {
226+
this.searchQuery = ''
227+
},
228+
148229
/**
149230
* Show picker and fetch for recommendations
150231
* Cache the circleId in case the url change or something
@@ -260,18 +341,9 @@ export default defineComponent({
260341
}
261342
}
262343
263-
.member-grid {
264-
display: grid;
265-
grid-template-columns: repeat(2, 1fr);
266-
gap: 8px;
267-
268-
@media (max-width: 768px) {
269-
grid-template-columns: 1fr;
270-
}
271-
272-
@media (min-width: 1200px) {
273-
grid-template-columns: repeat(3, 1fr);
274-
}
344+
.member-scroller {
345+
height: 100%;
346+
max-height: 200px;
275347
}
276348
277349
.empty-content {

src/store/circles.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ const mutations = {
8787
*/
8888
deleteMemberFromCircle(state, member) {
8989
// Circles dependencies are managed directly from the model
90-
member.delete()
90+
const singleId = member.singleId
91+
if (member.circle._members[singleId]) {
92+
Vue.delete(member.circle._members, singleId)
93+
}
9194
},
9295

9396
setCircleSettings(state, { circleId, settings }) {

0 commit comments

Comments
 (0)