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:
Pranav
2026-01-27 18:55:19 -08:00
committed by GitHub
parent 04b2901e1f
commit 7cddba2b08
11 changed files with 173 additions and 12 deletions

View File

@@ -45,14 +45,19 @@ export const handleContactOperationErrors = error => {
};
export const actions = {
search: async ({ commit }, { search, page, sortAttr, label }) => {
search: async (
{ commit },
{ search, page, sortAttr, label, append = false }
) => {
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
try {
const {
data: { payload, meta },
} = await ContactAPI.search(search, page, sortAttr, label);
commit(types.CLEAR_CONTACTS);
commit(types.SET_CONTACTS, payload);
if (!append) {
commit(types.CLEAR_CONTACTS);
}
commit(append ? types.APPEND_CONTACTS : types.SET_CONTACTS, payload);
commit(types.SET_CONTACT_META, meta);
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
} catch (error) {

View File

@@ -6,6 +6,7 @@ const state = {
meta: {
count: 0,
currentPage: 1,
hasMore: false,
},
records: {},
uiFlags: {

View File

@@ -15,9 +15,24 @@ export const mutations = {
},
[types.SET_CONTACT_META]: ($state, data) => {
const { count, current_page: currentPage } = data;
const { count, current_page: currentPage, has_more: hasMore } = data;
$state.meta.count = count;
$state.meta.currentPage = currentPage;
if (hasMore !== undefined) {
$state.meta.hasMore = hasMore;
}
},
[types.APPEND_CONTACTS]: ($state, data) => {
data.forEach(contact => {
$state.records[contact.id] = {
...($state.records[contact.id] || {}),
...contact,
};
if (!$state.sortOrder.includes(contact.id)) {
$state.sortOrder.push(contact.id);
}
});
},
[types.SET_CONTACTS]: ($state, data) => {