Skip to content

Commit fe5d83d

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

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"
@@ -52,11 +80,16 @@
5280
import {
5381
NcEmptyContent,
5482
isMobile,
83+
NcLoadingIcon as IconLoading,
84+
NcTextField
5585
} from '@nextcloud/vue'
86+
import { RecycleScroller } from 'vue-virtual-scroller'
87+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
5688
5789
import MemberGridItem from './MemberGridItem.vue'
5890
import EntityPicker from '../EntityPicker/EntityPicker.vue'
5991
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
92+
import IconSearch from 'vue-material-design-icons/Magnify.vue'
6093
6194
import RouterMixin from '../../mixins/RouterMixin.js'
6295
@@ -73,8 +106,12 @@ export default defineComponent({
73106
components: {
74107
EntityPicker,
75108
IconContact,
109+
IconLoading,
110+
IconSearch,
76111
MemberGridItem,
77112
NcEmptyContent,
113+
RecycleScroller,
114+
NcTextField,
78115
},
79116
80117
mixins: [isMobile, RouterMixin],
@@ -102,6 +139,8 @@ export default defineComponent({
102139
pickerData: [],
103140
pickerSelection: {},
104141
pickerTypes: CIRCLES_MEMBER_GROUPING,
142+
windowWidth: window.innerWidth,
143+
searchQuery: '',
105144
}
106145
},
107146
@@ -133,16 +172,58 @@ export default defineComponent({
133172
return [...teams, ...users]
134173
},
135174
175+
filteredList() {
176+
const query = this.searchQuery.toLowerCase()
177+
178+
return this.flatList.filter(member =>
179+
!this.searchQuery || member.displayName.toLowerCase().includes(query)
180+
);
181+
},
182+
136183
hasMembers() {
137184
return this.flatList.length > 0
138185
},
186+
187+
gridItems() {
188+
if (this.windowWidth < 768) {
189+
// undefined means that the grid will be rendered as a list
190+
return undefined
191+
}
192+
193+
return 2
194+
},
195+
196+
itemSecondarySize() {
197+
if (this.windowWidth < 768) {
198+
// undefined means that the grid will be rendered as a list
199+
return undefined
200+
}
201+
202+
// The maximum width of the member list is 500px,
203+
// so with two columns, each column is 242px wide (with scroll)
204+
return 242
205+
},
139206
},
140207
141208
mounted() {
142209
subscribe('contacts:circles:append', this.onShowPicker)
210+
211+
window.addEventListener('resize', this.onResize)
212+
},
213+
214+
beforeDestroy() {
215+
window.removeEventListener('resize', this.onResize)
143216
},
144217
145218
methods: {
219+
onResize() {
220+
this.windowWidth = window.innerWidth
221+
},
222+
223+
clearSearchField() {
224+
this.searchQuery = ''
225+
},
226+
146227
/**
147228
* Show picker and fetch for recommendations
148229
* Cache the circleId in case the url change or something
@@ -253,18 +334,9 @@ export default defineComponent({
253334
}
254335
}
255336
256-
.member-grid {
257-
display: grid;
258-
grid-template-columns: repeat(2, 1fr);
259-
gap: 8px;
260-
261-
@media (max-width: 768px) {
262-
grid-template-columns: 1fr;
263-
}
264-
265-
@media (min-width: 1200px) {
266-
grid-template-columns: repeat(3, 1fr);
267-
}
337+
.member-scroller {
338+
height: 100%;
339+
max-height: 200px;
268340
}
269341
270342
.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)