Skip to content

Commit 41e0014

Browse files
authored
feat: implement infinite scrolling for project and collection dropdowns (#3094)
1 parent 72f3ad9 commit 41e0014

8 files changed

Lines changed: 245 additions & 144 deletions

File tree

backend/src/database/repositories/segmentRepository.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,8 @@ class SegmentRepository extends RepositoryBase<
723723
s.name,
724724
s.url,
725725
s.slug,
726-
s.description
726+
s.description,
727+
COUNT(*) OVER () AS "totalCount"
727728
FROM segments s
728729
WHERE s."grandparentSlug" IS NOT NULL
729730
AND s."parentSlug" IS NOT NULL
@@ -745,10 +746,10 @@ class SegmentRepository extends RepositoryBase<
745746
},
746747
)
747748

748-
const rows = subprojects
749-
749+
const rows = subprojects.map((i) => removeFieldsFromObject(i, 'totalCount'))
750+
const count = subprojects.length > 0 ? +subprojects[0].totalCount : 0
750751
return {
751-
count: 1,
752+
count,
752753
rows,
753754
limit: criteria.limit,
754755
offset: criteria.offset,

frontend/src/modules/admin/modules/collections/components/lf-collection-add.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ const props = defineProps<{
179179
180180
const activeTab = ref('details');
181181
const loading = ref(false);
182-
const submitLoading = ref(false);
183182
const form = reactive<CollectionFormModel>({
184183
name: '',
185184
description: '',
@@ -238,7 +237,6 @@ const onCancel = () => {
238237
};
239238
240239
const onSubmit = () => {
241-
submitLoading.value = true;
242240
const request: CollectionRequest = {
243241
name: form.name,
244242
description: form.description,

frontend/src/modules/admin/modules/collections/services/collections.service.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@ import { QueryFunction } from '@tanstack/vue-query';
44
import { CollectionModel, CollectionRequest } from '../models/collection.model';
55

66
export class CollectionsService {
7-
static async list(query: any) {
8-
const response = await authAxios.post(
9-
'/collections/query',
10-
query,
11-
);
12-
13-
return response.data;
14-
}
15-
167
query(
178
query: () => Record<string, string | number | object>,
189
): QueryFunction<Pagination<CollectionModel>> {

frontend/src/modules/admin/modules/insights-projects/components/add-details-tab/lf-insights-projects-add-collection-dropdown.vue

Lines changed: 85 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
:value="item.id"
1717
/>
1818
<el-option
19-
v-if="loading && collections.length > 0"
19+
v-if="isFetchingNextPage || isPending"
2020
:key="'loading'"
2121
label="Loading..."
2222
value=""
@@ -27,74 +27,94 @@
2727
</template>
2828

2929
<script setup lang="ts">
30-
import { debounce } from 'lodash';
3130
import {
32-
nextTick, onBeforeUnmount, onMounted, reactive, ref,
31+
computed,
32+
nextTick,
33+
onBeforeUnmount,
34+
onMounted,
35+
reactive,
36+
ref,
37+
watch,
3338
} from 'vue';
34-
import { CollectionsService } from '@/modules/admin/modules/collections/services/collections.service';
39+
import {
40+
COLLECTIONS_SERVICE,
41+
} from '@/modules/admin/modules/collections/services/collections.service';
3542
import Message from '@/shared/message/message';
36-
import { CollectionModel } from '../../../collections/models/collection.model';
43+
import { TanstackKey } from '@/shared/types/tanstack';
44+
import { QueryFunction, useInfiniteQuery } from '@tanstack/vue-query';
45+
import { Pagination } from '@/shared/types/Pagination';
46+
import { debounce } from 'lodash';
3747
import { InsightsProjectAddFormModel } from '../../models/insights-project-add-form.model';
48+
import { CollectionModel } from '../../../collections/models/collection.model';
3849
3950
const props = defineProps<{
4051
form: InsightsProjectAddFormModel;
4152
}>();
4253
4354
const cForm = reactive(props.form);
4455
45-
const loading = ref(false);
46-
const collections = ref<CollectionModel[]>([]);
47-
const page = ref(0);
48-
const pageSize = 20;
49-
const noMoreData = ref(false);
5056
const searchQuery = ref('');
5157
let scrollContainer: HTMLElement | null = null;
5258
53-
// Your API service method
54-
function fetchCollections(query = '', pageNum = 0) {
55-
loading.value = true;
56-
CollectionsService.list({
57-
filter: query
58-
? {
59-
name: {
60-
like: `%${query}%`,
61-
},
62-
}
63-
: {},
64-
offset: pageNum * pageSize,
65-
limit: pageSize,
66-
})
67-
.then((res: { rows: CollectionModel[], total: string }) => {
68-
const { rows } = res;
69-
const selectedItems = cForm.collections;
70-
if (pageNum === 0) {
71-
collections.value = [...selectedItems, ...rows].reduce((acc, item) => {
72-
if (!acc.find((i) => i.id === item.id)) acc.push(item);
73-
return acc;
74-
}, [] as CollectionModel[]);
75-
} else {
76-
collections.value = [...collections.value, ...rows].reduce((acc, item) => {
77-
if (!acc.find((i) => i.id === item.id)) acc.push(item);
78-
return acc;
79-
}, [] as CollectionModel[]);
80-
}
81-
82-
noMoreData.value = collections.value.length >= +res.total;
83-
})
84-
.catch(() => {
85-
Message.closeAll();
86-
Message.error('Failed to load collections');
87-
})
88-
.finally(() => {
89-
loading.value = false;
90-
});
91-
}
59+
const queryKey = computed(() => [
60+
TanstackKey.ADMIN_COLLECTIONS,
61+
searchQuery.value,
62+
]);
63+
const queryFn = COLLECTIONS_SERVICE.query(() => ({
64+
filter: searchQuery.value
65+
? {
66+
name: {
67+
like: `%${searchQuery.value}%`,
68+
},
69+
}
70+
: {},
71+
offset: 0,
72+
limit: 20,
73+
})) as QueryFunction<Pagination<CollectionModel>, readonly unknown[], unknown>;
74+
75+
const {
76+
data,
77+
isPending,
78+
isFetchingNextPage,
79+
fetchNextPage,
80+
hasNextPage,
81+
isSuccess,
82+
error,
83+
} = useInfiniteQuery<Pagination<CollectionModel>, Error>({
84+
queryKey,
85+
queryFn,
86+
getNextPageParam: (lastPage) => {
87+
const nextPage = lastPage.offset + lastPage.limit;
88+
const totalRows = lastPage.total || lastPage.count;
89+
return nextPage < totalRows ? nextPage : undefined;
90+
},
91+
initialPageParam: 0,
92+
});
93+
94+
const collections = computed((): CollectionModel[] => {
95+
if (isSuccess.value && data.value) {
96+
const selectedItems = cForm.collections;
97+
const rows = data.value.pages.reduce(
98+
(acc, page) => acc.concat(page.rows),
99+
[] as CollectionModel[],
100+
);
101+
return [...selectedItems, ...rows].reduce((acc, item) => {
102+
if (!acc.find((i) => i.id === item.id)) acc.push(item);
103+
return acc;
104+
}, [] as CollectionModel[]);
105+
}
106+
return [];
107+
});
108+
109+
watch(error, (err) => {
110+
if (err) {
111+
Message.error('Something went wrong while fetching collections');
112+
}
113+
});
92114
93115
// Debounced search input handler
94116
const debouncedSearch = debounce((query: string) => {
95-
page.value = 0;
96-
noMoreData.value = false;
97-
fetchCollections(query, page.value);
117+
searchQuery.value = query;
98118
}, 300);
99119
100120
function onSearchInput(query: string) {
@@ -109,18 +129,16 @@ function onScroll(e: Event) {
109129
110130
const target = e.target as HTMLElement;
111131
if (
112-
!loading.value
113-
&& !noMoreData.value
132+
!isFetchingNextPage.value
133+
&& hasNextPage.value
114134
&& target.scrollHeight - target.scrollTop - target.clientHeight < threshold
115135
) {
116-
page.value += 1;
117-
fetchCollections(searchQuery.value, page.value);
136+
fetchNextPage();
118137
}
119138
}
120139
121140
// Attach scroll listener after dropdown renders
122141
onMounted(() => {
123-
fetchCollections('', 0);
124142
nextTick(() => {
125143
scrollContainer = document.querySelector(
126144
'.collection-infinite-select-dropdown .el-select-dropdown__wrap',
@@ -145,9 +163,18 @@ export default {
145163
</script>
146164

147165
<style>
148-
149166
.collection-infinite-select-dropdown .el-select-dropdown__wrap {
150167
max-height: 200px;
151168
overflow: auto;
152169
}
170+
171+
.collection-infinite-select-dropdown .el-select-dropdown__item span {
172+
max-width: 100%;
173+
text-overflow: ellipsis;
174+
overflow: hidden;
175+
}
176+
177+
.el-select-dropdown {
178+
max-width: 552px;
179+
}
153180
</style>

0 commit comments

Comments
 (0)