Skip to content

Commit 66fb301

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

File tree

3 files changed

+100
-21
lines changed

3 files changed

+100
-21
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: 95 additions & 19 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"
@@ -50,11 +75,19 @@
5075
</template>
5176

5277
<script lang="ts">
53-
import { NcEmptyContent } from '@nextcloud/vue'
78+
import {
79+
NcEmptyContent,
80+
isMobile,
81+
NcLoadingIcon as IconLoading,
82+
NcTextField
83+
} from '@nextcloud/vue'
84+
import { RecycleScroller } from 'vue-virtual-scroller'
85+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
5486
5587
import MemberGridItem from './MemberGridItem.vue'
5688
import EntityPicker from '../EntityPicker/EntityPicker.vue'
5789
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
90+
import IconSearch from 'vue-material-design-icons/Magnify.vue'
5891
5992
import RouterMixin from '../../mixins/RouterMixin.js'
6093
@@ -72,8 +105,12 @@ export default defineComponent({
72105
components: {
73106
EntityPicker,
74107
IconContact,
108+
IconLoading,
109+
IconSearch,
75110
MemberGridItem,
76111
NcEmptyContent,
112+
RecycleScroller,
113+
NcTextField,
77114
},
78115
79116
mixins: [IsMobileMixin, RouterMixin],
@@ -101,6 +138,8 @@ export default defineComponent({
101138
pickerData: [],
102139
pickerSelection: {},
103140
pickerTypes: CIRCLES_MEMBER_GROUPING,
141+
windowWidth: window.innerWidth,
142+
searchQuery: '',
104143
}
105144
},
106145
@@ -132,17 +171,59 @@ export default defineComponent({
132171
return [...teams, ...users]
133172
},
134173
174+
filteredList() {
175+
const query = this.searchQuery.toLowerCase()
176+
177+
return this.flatList.filter(member =>
178+
!this.searchQuery || member.displayName.toLowerCase().includes(query)
179+
)
180+
},
181+
135182
hasMembers() {
136183
return this.flatList.length > 0
137184
},
185+
186+
gridItems() {
187+
if (this.windowWidth < 768) {
188+
// undefined means that the grid will be rendered as a list
189+
return undefined
190+
}
191+
192+
return 2
193+
},
194+
195+
itemSecondarySize() {
196+
if (this.windowWidth < 768) {
197+
// undefined means that the grid will be rendered as a list
198+
return undefined
199+
}
200+
201+
// The maximum width of the member list is 500px,
202+
// so with two columns, each column is 242px wide (with scroll)
203+
return 242
204+
},
138205
},
139206
140207
mounted() {
141208
subscribe('contacts:circles:append', this.onShowPicker)
142209
subscribe('guests:user:created', this.onGuestCreated)
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
@@ -258,21 +339,16 @@ export default defineComponent({
258339
}
259340
}
260341
261-
.member-grid {
262-
display: grid;
263-
grid-template-columns: repeat(2, 1fr);
264-
gap: 8px;
265-
266-
@media (max-width: 768px) {
267-
grid-template-columns: 1fr;
268-
}
269-
270-
@media (min-width: 1200px) {
271-
grid-template-columns: repeat(3, 1fr);
272-
}
342+
.member-scroller {
343+
height: 100%;
344+
max-height: 200px;
273345
}
274346
275347
.empty-content {
276348
height: 100%;
277349
}
350+
351+
.empty-search-results {
352+
margin-top: 2rem;
353+
}
278354
</style>

src/store/circles.js

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

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

0 commit comments

Comments
 (0)