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 >
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' ;
6073import { InsightsProjectModel } from ' @/modules/admin/modules/insights-projects/models/insights-project.model' ;
6174import 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' ;
6281import { useInsightsProjectsStore } from ' ../../insights-projects/pinia' ;
6382
6483const SearchIcon = h (
@@ -83,15 +102,52 @@ const emit = defineEmits<{(e: 'onAddProject', projectId: string): void }>();
83102const props = defineProps <{
84103 selectedProjects: InsightsProjectModel [];
85104}>();
105+ let scrollContainer: HTMLElement | null = null ;
86106
87107const insightsProjectsStore = useInsightsProjectsStore ();
88108
89109const inputRef = ref (null );
90110const searchQuery = ref (' ' );
111+ const searchValue = useDebounce (searchQuery , 300 );
91112const isPopoverVisible = ref (false );
92113const 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
96152const 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">
0 commit comments