fix: Use search API instead of filter in the filter in the endpoints (#13651)

- Replace `POST /contacts/filter` with `GET /contacts/search` for
contact lookup in compose new conversation
- Remove client-side input-type detection logic (`generateContactQuery`,
key filtering by email/phone/name) — the search API handles matching
across name, email, phone_number, and identifier server-side via a
single `ILIKE` query
- Filter the contacts with emails in cc and bcc fields.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Pranav
2026-02-25 09:08:24 -08:00
committed by GitHub
parent efe49f7da4
commit 9fab70aebf
5 changed files with 27 additions and 148 deletions

View File

@@ -158,21 +158,7 @@ const isAnyDropdownActive = computed(() => {
const handleContactSearch = value => { const handleContactSearch = value => {
showContactsDropdown.value = true; showContactsDropdown.value = true;
const query = typeof value === 'string' ? value.trim() : ''; emit('searchContacts', value);
const hasAlphabet = Array.from(query).some(char => {
const lower = char.toLowerCase();
const upper = char.toUpperCase();
return lower !== upper;
});
const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query);
const keys = ['email', 'phone_number', 'name'].filter(key => {
if (key === 'phone_number' && hasAlphabet) return false;
if (key === 'name' && isEmailLike) return false;
return true;
});
emit('searchContacts', { keys, query: value });
}; };
const handleDropdownUpdate = (type, value) => { const handleDropdownUpdate = (type, value) => {
@@ -187,12 +173,12 @@ const handleDropdownUpdate = (type, value) => {
const searchCcEmails = value => { const searchCcEmails = value => {
showCcEmailsDropdown.value = true; showCcEmailsDropdown.value = true;
emit('searchContacts', { keys: ['email'], query: value }); emit('searchContacts', value);
}; };
const searchBccEmails = value => { const searchBccEmails = value => {
showBccEmailsDropdown.value = true; showBccEmailsDropdown.value = true;
emit('searchContacts', { keys: ['email'], query: value }); emit('searchContacts', value);
}; };
const setSelectedContact = async ({ value, action, ...rest }) => { const setSelectedContact = async ({ value, action, ...rest }) => {

View File

@@ -44,14 +44,16 @@ const bccEmailsArray = computed(() =>
); );
const contactEmailsList = computed(() => { const contactEmailsList = computed(() => {
return props.contacts?.map(({ name, id, email }) => ({ return props.contacts
id, ?.filter(contact => contact.email)
label: email, .map(({ name, id, email }) => ({
email, id,
thumbnail: { name: name, src: '' }, label: email,
value: id, email,
action: 'email', thumbnail: { name: name, src: '' },
})); value: id,
action: 'email',
}));
}); });
// Handle updates from TagInput and convert array back to string // Handle updates from TagInput and convert array back to string

View File

@@ -176,32 +176,14 @@ export const prepareWhatsAppMessagePayload = ({
}; };
}; };
export const generateContactQuery = ({ keys = ['email'], query }) => {
return {
payload: keys.map(key => {
const filterPayload = {
attribute_key: key,
filter_operator: 'contains',
values: [query],
attribute_model: 'standard',
};
if (keys.findIndex(k => k === key) !== keys.length - 1) {
filterPayload.query_operator = 'or';
}
return filterPayload;
}),
};
};
// API Calls // API Calls
export const searchContacts = async ({ keys, query }) => { export const searchContacts = async query => {
const trimmed = typeof query === 'string' ? query.trim() : '';
if (!trimmed) return [];
const { const {
data: { payload }, data: { payload },
} = await ContactAPI.filter( } = await ContactAPI.search(trimmed);
undefined,
'name',
generateContactQuery({ keys, query })
);
const camelCasedPayload = camelcaseKeys(payload, { deep: true }); const camelCasedPayload = camelcaseKeys(payload, { deep: true });
// Filter contacts that have either phone_number or email // Filter contacts that have either phone_number or email
const filteredPayload = camelCasedPayload?.filter( const filteredPayload = camelCasedPayload?.filter(

View File

@@ -336,70 +336,6 @@ describe('composeConversationHelper', () => {
}); });
}); });
describe('generateContactQuery', () => {
it('generates correct query structure for contact search', () => {
const query = 'test@example.com';
const expected = {
payload: [
{
attribute_key: 'email',
filter_operator: 'contains',
values: [query],
attribute_model: 'standard',
},
],
};
expect(helpers.generateContactQuery({ keys: ['email'], query })).toEqual(
expected
);
});
it('handles empty query', () => {
const expected = {
payload: [
{
attribute_key: 'email',
filter_operator: 'contains',
values: [''],
attribute_model: 'standard',
},
],
};
expect(
helpers.generateContactQuery({ keys: ['email'], query: '' })
).toEqual(expected);
});
it('handles mutliple keys', () => {
const expected = {
payload: [
{
attribute_key: 'email',
filter_operator: 'contains',
values: ['john'],
attribute_model: 'standard',
query_operator: 'or',
},
{
attribute_key: 'phone_number',
filter_operator: 'contains',
values: ['john'],
attribute_model: 'standard',
},
],
};
expect(
helpers.generateContactQuery({
keys: ['email', 'phone_number'],
query: 'john',
})
).toEqual(expected);
});
});
describe('API calls', () => { describe('API calls', () => {
describe('searchContacts', () => { describe('searchContacts', () => {
it('searches contacts and returns camelCase results', async () => { it('searches contacts and returns camelCase results', async () => {
@@ -413,14 +349,11 @@ describe('composeConversationHelper', () => {
}, },
]; ];
ContactAPI.filter.mockResolvedValue({ ContactAPI.search.mockResolvedValue({
data: { payload: mockPayload }, data: { payload: mockPayload },
}); });
const result = await helpers.searchContacts({ const result = await helpers.searchContacts('john');
keys: ['email'],
query: 'john',
});
expect(result).toEqual([ expect(result).toEqual([
{ {
@@ -432,16 +365,7 @@ describe('composeConversationHelper', () => {
}, },
]); ]);
expect(ContactAPI.filter).toHaveBeenCalledWith(undefined, 'name', { expect(ContactAPI.search).toHaveBeenCalledWith('john');
payload: [
{
attribute_key: 'email',
filter_operator: 'contains',
values: ['john'],
attribute_model: 'standard',
},
],
});
}); });
it('searches contacts and returns only contacts with email or phone number', async () => { it('searches contacts and returns only contacts with email or phone number', async () => {
@@ -469,14 +393,11 @@ describe('composeConversationHelper', () => {
}, },
]; ];
ContactAPI.filter.mockResolvedValue({ ContactAPI.search.mockResolvedValue({
data: { payload: mockPayload }, data: { payload: mockPayload },
}); });
const result = await helpers.searchContacts({ const result = await helpers.searchContacts('john');
keys: ['email'],
query: 'john',
});
// Should only return contacts with either email or phone number // Should only return contacts with either email or phone number
expect(result).toEqual([ expect(result).toEqual([
@@ -496,20 +417,11 @@ describe('composeConversationHelper', () => {
}, },
]); ]);
expect(ContactAPI.filter).toHaveBeenCalledWith(undefined, 'name', { expect(ContactAPI.search).toHaveBeenCalledWith('john');
payload: [
{
attribute_key: 'email',
filter_operator: 'contains',
values: ['john'],
attribute_model: 'standard',
},
],
});
}); });
it('handles empty search results', async () => { it('handles empty search results', async () => {
ContactAPI.filter.mockResolvedValue({ ContactAPI.search.mockResolvedValue({
data: { payload: [] }, data: { payload: [] },
}); });
@@ -536,7 +448,7 @@ describe('composeConversationHelper', () => {
}, },
]; ];
ContactAPI.filter.mockResolvedValue({ ContactAPI.search.mockResolvedValue({
data: { payload: mockPayload }, data: { payload: mockPayload },
}); });

View File

@@ -119,10 +119,7 @@ const debouncedSearch = debounce(async query => {
} }
try { try {
const contacts = await searchContacts({ const contacts = await searchContacts(query);
keys: ['name', 'email', 'phone_number'],
query,
});
// Add selected contact to top if not already in results // Add selected contact to top if not already in results
const allContacts = selectedContact.value const allContacts = selectedContact.value