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:
@@ -36,6 +36,9 @@ const meta = useMapGetter('contacts/getMeta');
|
||||
const searchQuery = computed(() => route.query?.search);
|
||||
const searchValue = ref(searchQuery.value || '');
|
||||
const pageNumber = computed(() => Number(route.query?.page) || 1);
|
||||
// For infinite scroll in search, track page internally
|
||||
const searchPageNumber = ref(1);
|
||||
const isLoadingMore = ref(false);
|
||||
|
||||
const parseSortSettings = (sortString = '') => {
|
||||
const hasDescending = sortString.startsWith('-');
|
||||
@@ -62,6 +65,8 @@ const isFetchingList = computed(
|
||||
);
|
||||
const currentPage = computed(() => Number(meta.value?.currentPage));
|
||||
const totalItems = computed(() => meta.value?.count);
|
||||
const hasMore = computed(() => meta.value?.hasMore ?? false);
|
||||
const isSearchView = computed(() => !!searchQuery.value);
|
||||
|
||||
const selectedContactIds = ref([]);
|
||||
const isBulkActionLoading = ref(false);
|
||||
@@ -212,8 +217,11 @@ const fetchActiveContacts = async (page = 1) => {
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
const searchContacts = debounce(async (value, page = 1) => {
|
||||
clearSelection();
|
||||
const searchContacts = debounce(async (value, page = 1, append = false) => {
|
||||
if (!append) {
|
||||
clearSelection();
|
||||
searchPageNumber.value = 1;
|
||||
}
|
||||
await store.dispatch('contacts/clearContactFilters');
|
||||
searchValue.value = value;
|
||||
|
||||
@@ -227,9 +235,27 @@ const searchContacts = debounce(async (value, page = 1) => {
|
||||
await store.dispatch('contacts/search', {
|
||||
...getCommonFetchParams(page),
|
||||
search: encodeURIComponent(value),
|
||||
append,
|
||||
});
|
||||
searchPageNumber.value = page;
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
const loadMoreSearchResults = async () => {
|
||||
if (!hasMore.value || isLoadingMore.value) return;
|
||||
|
||||
isLoadingMore.value = true;
|
||||
const nextPage = searchPageNumber.value + 1;
|
||||
|
||||
await store.dispatch('contacts/search', {
|
||||
...getCommonFetchParams(nextPage),
|
||||
search: encodeURIComponent(searchValue.value),
|
||||
append: true,
|
||||
});
|
||||
|
||||
searchPageNumber.value = nextPage;
|
||||
isLoadingMore.value = false;
|
||||
};
|
||||
|
||||
const fetchContactsBasedOnContext = async page => {
|
||||
clearSelection();
|
||||
updatePageParam(page, searchValue.value);
|
||||
@@ -416,21 +442,25 @@ onMounted(async () => {
|
||||
:header-title="headerTitle"
|
||||
:current-page="currentPage"
|
||||
:total-items="totalItems"
|
||||
:show-pagination-footer="!isFetchingList && hasContacts"
|
||||
:show-pagination-footer="!isFetchingList && hasContacts && !isSearchView"
|
||||
:active-sort="sortState.activeSort"
|
||||
:active-ordering="sortState.activeOrdering"
|
||||
:active-segment="activeSegment"
|
||||
:segments-id="activeSegmentId"
|
||||
:is-fetching-list="isFetchingList"
|
||||
:has-applied-filters="hasAppliedFilters"
|
||||
:use-infinite-scroll="isSearchView"
|
||||
:has-more="hasMore"
|
||||
:is-loading-more="isLoadingMore"
|
||||
@update:current-page="fetchContactsBasedOnContext"
|
||||
@search="searchContacts"
|
||||
@update:sort="handleSort"
|
||||
@apply-filter="fetchSavedOrAppliedFilteredContact"
|
||||
@clear-filters="fetchContacts"
|
||||
@load-more="loadMoreSearchResults"
|
||||
>
|
||||
<div
|
||||
v-if="isFetchingList"
|
||||
v-if="isFetchingList && !(isSearchView && hasContacts)"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
|
||||
Reference in New Issue
Block a user