feat: Add infinite scroll to contacts search page (#13376)
## Summary - Add `has_more` to contacts search API response to enable infinite scroll without expensive count queries - Set `count` to the number of items in the current page instead of total count - Implement "Load more" button for contacts search results - Keep existing contacts visible while loading additional pages ## Changes ### Backend - Add `fetch_contacts_with_has_more` method that fetches N+1 records to determine if more pages exist - Return `has_more` in search endpoint meta response - Set `count` to current page size instead of total count ### Frontend - Add `APPEND_CONTACTS` mutation for appending contacts without clearing existing ones - Update search action to support `append` parameter - Add `ContactsLoadMore` component with loading state - Update `ContactsListLayout` to support infinite scroll mode - Update `ContactsIndex` to use infinite scroll for search view
This commit is contained in:
@@ -5,6 +5,7 @@ import { useRoute } from 'vue-router';
|
||||
import ContactListHeaderWrapper from 'dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue';
|
||||
import ContactsActiveFiltersPreview from 'dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue';
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
import ContactsLoadMore from 'dashboard/components-next/Contacts/ContactsLoadMore.vue';
|
||||
|
||||
const props = defineProps({
|
||||
searchValue: { type: String, default: '' },
|
||||
@@ -19,6 +20,9 @@ const props = defineProps({
|
||||
segmentsId: { type: [String, Number], default: 0 },
|
||||
hasAppliedFilters: { type: Boolean, default: false },
|
||||
isFetchingList: { type: Boolean, default: false },
|
||||
useInfiniteScroll: { type: Boolean, default: false },
|
||||
hasMore: { type: Boolean, default: false },
|
||||
isLoadingMore: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -27,6 +31,7 @@ const emit = defineEmits([
|
||||
'search',
|
||||
'applyFilter',
|
||||
'clearFilters',
|
||||
'loadMore',
|
||||
]);
|
||||
|
||||
const route = useRoute();
|
||||
@@ -61,6 +66,14 @@ const updateCurrentPage = page => {
|
||||
const openFilter = () => {
|
||||
contactListHeaderWrapper.value?.onToggleFilters();
|
||||
};
|
||||
|
||||
const showLoadMore = computed(() => {
|
||||
return props.useInfiniteScroll && props.hasMore;
|
||||
});
|
||||
|
||||
const showPagination = computed(() => {
|
||||
return !props.useInfiniteScroll && props.showPaginationFooter;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -94,9 +107,14 @@ const openFilter = () => {
|
||||
@open-filter="openFilter"
|
||||
/>
|
||||
<slot name="default" />
|
||||
<ContactsLoadMore
|
||||
v-if="showLoadMore"
|
||||
:is-loading="isLoadingMore"
|
||||
@load-more="emit('loadMore')"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||
<footer v-if="showPagination" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||
<PaginationFooter
|
||||
current-page-info="CONTACTS_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||
:current-page="currentPage"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['loadMore']);
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center py-4">
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.LOAD_MORE')"
|
||||
:is-loading="isLoading"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
size="sm"
|
||||
@click="emit('loadMore')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user