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:
@@ -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 }) => {
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ const bccEmailsArray = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const contactEmailsList = computed(() => {
|
const contactEmailsList = computed(() => {
|
||||||
return props.contacts?.map(({ name, id, email }) => ({
|
return props.contacts
|
||||||
|
?.filter(contact => contact.email)
|
||||||
|
.map(({ name, id, email }) => ({
|
||||||
id,
|
id,
|
||||||
label: email,
|
label: email,
|
||||||
email,
|
email,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user