Skip to content

Commit f000cbc

Browse files
authored
feat: implement infinite scroll and loading state for insights projects list (#3055)
1 parent 041140e commit f000cbc

6 files changed

Lines changed: 127 additions & 10 deletions

File tree

frontend/components.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ declare module '@vue/runtime-core' {
3232
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
3333
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
3434
ElSelect: typeof import('element-plus/es')['ElSelect']
35-
ElTable: typeof import('element-plus/es')['ElTable']
36-
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
3735
ElTabPane: typeof import('element-plus/es')['ElTabPane']
3836
ElTabs: typeof import('element-plus/es')['ElTabs']
3937
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,8 @@ import {
165165
CollectionModel,
166166
CollectionRequest,
167167
} from '../models/collection.model';
168-
import { InsightsProjectsService } from '../../insights-projects/services/insights-projects.service';
169-
import { useInsightsProjectsStore } from '../../insights-projects/pinia';
170168
import { COLLECTIONS_SERVICE } from '../services/collections.service';
171169
172-
const insightsProjectsStore = useInsightsProjectsStore();
173-
174170
const emit = defineEmits<{(e: 'update:modelValue', value: boolean): void;
175171
(e: 'onCollectionEdited'): void;
176172
(e: 'onCollectionCreated'): void;
@@ -227,9 +223,6 @@ const fillForm = (record?: CollectionModel) => {
227223
};
228224
229225
onMounted(() => {
230-
InsightsProjectsService.list({}).then((response) => {
231-
insightsProjectsStore.setInsightsProjects(response.rows);
232-
});
233226
if (isEditForm.value) {
234227
loading.value = true;
235228
fillForm(props.collection);

frontend/src/modules/admin/modules/collections/components/lf-insights-projects-list-dropdown.vue

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,19 @@
4646
/>
4747
<span class="ml-2 text-gray-900 text-sm line-clamp-2">{{ project.name }}</span>
4848
</div>
49+
<div
50+
v-if="isFetchingNextPage"
51+
class="text-gray-400 px-3 h-20 flex items-center justify-center"
52+
>
53+
<lf-icon name="circle-notch" class="animate-spin text-gray-400" :size="16" />
54+
<span class="text-tiny ml-1 text-gray-400">Loading projects...</span>
55+
</div>
4956
</div>
5057
</div>
58+
<div v-else-if="isPending" class="text-gray-400 px-3 h-20 flex items-center justify-center">
59+
<lf-icon name="circle-notch" class="animate-spin text-gray-400" :size="16" />
60+
<span class="text-tiny ml-1 text-gray-400">Loading projects...</span>
61+
</div>
5162
<div v-else class="text-gray-400 px-3 h-10 flex items-center">
5263
No projects found
5364
</div>
@@ -56,9 +67,17 @@
5667
</template>
5768

5869
<script setup lang="ts">
59-
import { h, ref, computed } from 'vue';
70+
import {
71+
h, ref, computed, onMounted, nextTick, watch, onBeforeUnmount,
72+
} from 'vue';
6073
import { InsightsProjectModel } from '@/modules/admin/modules/insights-projects/models/insights-project.model';
6174
import LfAvatar from '@/ui-kit/avatar/Avatar.vue';
75+
import { QueryFunction, useInfiniteQuery } from '@tanstack/vue-query';
76+
import { useDebounce } from '@vueuse/core';
77+
import { Pagination } from '@/shared/types/Pagination';
78+
import { TanstackKey } from '@/shared/types/tanstack';
79+
import Message from '@/shared/message/message';
80+
import { INSIGHTS_PROJECTS_SERVICE } from '../../insights-projects/services/insights-projects.service';
6281
import { useInsightsProjectsStore } from '../../insights-projects/pinia';
6382
6483
const SearchIcon = h(
@@ -83,15 +102,52 @@ const emit = defineEmits<{(e: 'onAddProject', projectId: string): void }>();
83102
const props = defineProps<{
84103
selectedProjects: InsightsProjectModel[];
85104
}>();
105+
let scrollContainer: HTMLElement | null = null;
86106
87107
const insightsProjectsStore = useInsightsProjectsStore();
88108
89109
const inputRef = ref(null);
90110
const searchQuery = ref('');
111+
const searchValue = useDebounce(searchQuery, 300);
91112
const isPopoverVisible = ref(false);
92113
const displayProjects = computed(() => removeSelectedProject(
93114
insightsProjectsStore.searchInsightsProjects(searchQuery.value),
94115
));
116+
const queryKey = computed(() => [
117+
TanstackKey.ADMIN_INSIGHTS_PROJECTS,
118+
searchValue.value,
119+
]);
120+
121+
const projectGroupsQueryFn = INSIGHTS_PROJECTS_SERVICE.query(() => ({
122+
limit: 20,
123+
offset: 0,
124+
filter: searchValue.value
125+
? {
126+
name: {
127+
like: `%${searchValue.value}%`,
128+
},
129+
}
130+
: {},
131+
})) as QueryFunction<Pagination<InsightsProjectModel>, readonly unknown[], unknown>;
132+
133+
const {
134+
data,
135+
isPending,
136+
isFetchingNextPage,
137+
fetchNextPage,
138+
hasNextPage,
139+
isSuccess,
140+
error,
141+
} = useInfiniteQuery<Pagination<InsightsProjectModel>, Error>({
142+
queryKey,
143+
queryFn: projectGroupsQueryFn,
144+
getNextPageParam: (lastPage) => {
145+
const nextPage = lastPage.offset + lastPage.limit;
146+
const totalRows = lastPage.total!;
147+
return nextPage < totalRows ? nextPage : undefined;
148+
},
149+
initialPageParam: 0,
150+
});
95151
96152
const removeSelectedProject = (projects: InsightsProjectModel[]) => {
97153
const selectedProjectsIds = props.selectedProjects.map(
@@ -115,6 +171,59 @@ const onOptionClick = (project: InsightsProjectModel) => {
115171
116172
emit('onAddProject', project.id);
117173
};
174+
175+
// Infinite scroll handler
176+
function onScroll(e: Event) {
177+
if (!scrollContainer) return;
178+
const threshold = 20;
179+
180+
const target = e.target as HTMLElement;
181+
if (
182+
!isFetchingNextPage.value
183+
&& hasNextPage.value
184+
&& target.scrollHeight - target.scrollTop - target.clientHeight < threshold
185+
) {
186+
fetchNextPage();
187+
}
188+
}
189+
190+
watch(data, () => {
191+
if (isSuccess.value && data.value) {
192+
let result = data.value.pages.reduce(
193+
(acc, page) => acc.concat(page.rows),
194+
[] as InsightsProjectModel[],
195+
);
196+
197+
result = [...props.selectedProjects, ...result].reduce((acc, item) => {
198+
if (!acc.find((i) => i.id === item.id)) acc.push(item);
199+
return acc;
200+
}, [] as InsightsProjectModel[]);
201+
insightsProjectsStore.setInsightsProjects(result);
202+
}
203+
}, { immediate: true });
204+
205+
watch(error, (err) => {
206+
if (err) {
207+
Message.error('Something went wrong while fetching Insights projects');
208+
}
209+
});
210+
211+
onMounted(() => {
212+
nextTick(() => {
213+
scrollContainer = document.querySelector(
214+
'.insights-projects-select-popper',
215+
);
216+
if (scrollContainer) {
217+
scrollContainer.addEventListener('scroll', onScroll);
218+
}
219+
});
220+
});
221+
222+
onBeforeUnmount(() => {
223+
if (scrollContainer) {
224+
scrollContainer.removeEventListener('scroll', onScroll);
225+
}
226+
});
118227
</script>
119228

120229
<script lang="ts">

frontend/src/modules/admin/modules/insights-projects/services/insights-projects.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import authAxios from '@/shared/axios/auth-axios';
2+
import { Pagination } from '@/shared/types/Pagination';
3+
import { QueryFunction } from '@tanstack/vue-query';
4+
import { InsightsProjectModel } from '../models/insights-project.model';
25

36
export class InsightsProjectsService {
47
static async list(query: any) {
@@ -9,6 +12,17 @@ export class InsightsProjectsService {
912
return response.data;
1013
}
1114

15+
query(
16+
query: () => Record<string, string | number | object>,
17+
): QueryFunction<Pagination<InsightsProjectModel>> {
18+
return ({ pageParam = 0 }) => authAxios
19+
.post<Pagination<InsightsProjectModel>>('/collections/insights-projects/query', {
20+
...query(),
21+
offset: pageParam,
22+
})
23+
.then((res) => res.data);
24+
}
25+
1226
static async getById(id: string) {
1327
const response = await authAxios.get(
1428
`/collections/insights-projects/${id}`,
@@ -53,3 +67,4 @@ export class InsightsProjectsService {
5367
return response.data;
5468
}
5569
}
70+
export const INSIGHTS_PROJECTS_SERVICE = new InsightsProjectsService();

frontend/src/shared/types/Pagination.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface Pagination<T>{
22
count: number;
3+
total?: number;
34
limit: number;
45
offset: number;
56
rows: T[]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export enum TanstackKey {
22
ADMIN_PROJECT_GROUPS = 'admin-project-groups',
33
ADMIN_COLLECTIONS = 'admin-collections',
4+
ADMIN_INSIGHTS_PROJECTS = 'admin-insights-projects',
45
}

0 commit comments

Comments
 (0)