Skip to content

Commit 0427db1

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

File tree

3 files changed

+96
-20
lines changed

3 files changed

+96
-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: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,37 @@
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 v-if="flatList.length > 20"
32+
v-model="searchQuery"
33+
:label="t('contacts', 'Search among current members')"
34+
trailing-button-icon="close"
35+
:show-trailing-button="searchQuery !== ''"
36+
@trailing-button-click="clearSearchField">
37+
<IconSearch :size="20" />
38+
</NcTextField>
39+
<RecycleScroller ref="scroller"
40+
class="member-scroller"
41+
:items="filteredList"
42+
:item-size="56"
43+
:grid-items="gridItems"
44+
:item-secondary-size="itemSecondarySize">
45+
<template #default="{ item }">
46+
<MemberGridItem :key="`member-grid-item-${item.id}`"
47+
:member="item"
48+
:is-team="!item.isUser" />
49+
</template>
50+
<template #empty v-if="!filteredList.length">
51+
<div class="empty-search-results">
52+
<NcEmptyContent :name="t('contacts', 'No results found')">
53+
<template #icon>
54+
<IconSearch :size="20" />
55+
</template>
56+
</NcEmptyContent>
57+
</div>
58+
</template>
59+
</RecycleScroller>
60+
</template>
3661

3762
<!-- member picker -->
3863
<EntityPicker v-if="showPicker"
@@ -53,11 +78,16 @@
5378
import {
5479
NcEmptyContent,
5580
isMobile,
81+
NcLoadingIcon as IconLoading,
82+
NcTextField
5683
} from '@nextcloud/vue'
84+
import { RecycleScroller } from 'vue-virtual-scroller'
85+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
5786
5887
import MemberGridItem from './MemberGridItem.vue'
5988
import EntityPicker from '../EntityPicker/EntityPicker.vue'
6089
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
90+
import IconSearch from 'vue-material-design-icons/Magnify.vue'
6191
6292
import RouterMixin from '../../mixins/RouterMixin.js'
6393
@@ -74,8 +104,12 @@ export default defineComponent({
74104
components: {
75105
EntityPicker,
76106
IconContact,
107+
IconLoading,
108+
IconSearch,
77109
MemberGridItem,
78110
NcEmptyContent,
111+
RecycleScroller,
112+
NcTextField,
79113
},
80114
81115
mixins: [isMobile, RouterMixin],
@@ -103,6 +137,8 @@ export default defineComponent({
103137
pickerData: [],
104138
pickerSelection: {},
105139
pickerTypes: CIRCLES_MEMBER_GROUPING,
140+
windowWidth: window.innerWidth,
141+
searchQuery: '',
106142
}
107143
},
108144
@@ -134,17 +170,59 @@ export default defineComponent({
134170
return [...teams, ...users]
135171
},
136172
173+
filteredList() {
174+
const query = this.searchQuery.toLowerCase()
175+
176+
return this.flatList.filter(member =>
177+
!this.searchQuery || member.displayName.toLowerCase().includes(query)
178+
)
179+
},
180+
137181
hasMembers() {
138182
return this.flatList.length > 0
139183
},
184+
185+
gridItems() {
186+
if (this.windowWidth < 768) {
187+
// undefined means that the grid will be rendered as a list
188+
return undefined
189+
}
190+
191+
return 2
192+
},
193+
194+
itemSecondarySize() {
195+
if (this.windowWidth < 768) {
196+
// undefined means that the grid will be rendered as a list
197+
return undefined
198+
}
199+
200+
// The maximum width of the member list is 500px,
201+
// so with two columns, each column is 242px wide (with scroll)
202+
return 242
203+
},
140204
},
141205
142206
mounted() {
143207
subscribe('contacts:circles:append', this.onShowPicker)
144208
subscribe('guests:user:created', this.onGuestCreated)
209+
210+
window.addEventListener('resize', this.onResize)
211+
},
212+
213+
beforeDestroy() {
214+
window.removeEventListener('resize', this.onResize)
145215
},
146216
147217
methods: {
218+
onResize() {
219+
this.windowWidth = window.innerWidth
220+
},
221+
222+
clearSearchField() {
223+
this.searchQuery = ''
224+
},
225+
148226
/**
149227
* Show picker and fetch for recommendations
150228
* Cache the circleId in case the url change or something
@@ -260,21 +338,16 @@ export default defineComponent({
260338
}
261339
}
262340
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-
}
341+
.member-scroller {
342+
height: 100%;
343+
max-height: 200px;
275344
}
276345
277346
.empty-content {
278347
height: 100%;
279348
}
349+
350+
.empty-search-results {
351+
margin-top: 2rem;
352+
}
280353
</style>

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)