diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue b/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
index 764f46141..ad007af1c 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue
@@ -1,6 +1,7 @@
-
+
{
});
const selectedContactLabel = computed(() => {
- return `${props.selectedContact?.name} (${props.selectedContact?.email})`;
+ const { name, email = '', phoneNumber = '' } = props.selectedContact || {};
+ if (email) {
+ return `${name} (${email})`;
+ }
+ if (phoneNumber) {
+ return `${name} (${phoneNumber})`;
+ }
+ return name || '';
});
const errorClass = computed(() => {
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
index ef3d0e88d..c6bb9b526 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
@@ -154,7 +154,12 @@ export const searchContacts = async ({ keys, query }) => {
'name',
generateContactQuery({ keys, query })
);
- return camelcaseKeys(payload, { deep: true });
+ const camelCasedPayload = camelcaseKeys(payload, { deep: true });
+ // Filter contacts that have either phone_number or email
+ const filteredPayload = camelCasedPayload?.filter(
+ contact => contact.phoneNumber || contact.email
+ );
+ return filteredPayload || [];
};
export const createNewContact = async email => {
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
index ca30235d1..54c4eab60 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/specs/composeConversationHelper.spec.js
@@ -297,6 +297,70 @@ describe('composeConversationHelper', () => {
});
});
+ it('searches contacts and returns only contacts with email or phone number', async () => {
+ const mockPayload = [
+ {
+ id: 1,
+ name: 'John Doe',
+ email: 'john@example.com',
+ phone_number: '+1234567890',
+ created_at: '2023-01-01',
+ },
+ {
+ id: 2,
+ name: 'Jane Doe',
+ email: null,
+ phone_number: null,
+ created_at: '2023-01-01',
+ },
+ {
+ id: 3,
+ name: 'Bob Smith',
+ email: 'bob@example.com',
+ phone_number: null,
+ created_at: '2023-01-01',
+ },
+ ];
+
+ ContactAPI.filter.mockResolvedValue({
+ data: { payload: mockPayload },
+ });
+
+ const result = await helpers.searchContacts({
+ keys: ['email'],
+ query: 'john',
+ });
+
+ // Should only return contacts with either email or phone number
+ expect(result).toEqual([
+ {
+ id: 1,
+ name: 'John Doe',
+ email: 'john@example.com',
+ phoneNumber: '+1234567890',
+ createdAt: '2023-01-01',
+ },
+ {
+ id: 3,
+ name: 'Bob Smith',
+ email: 'bob@example.com',
+ phoneNumber: null,
+ createdAt: '2023-01-01',
+ },
+ ]);
+
+ expect(ContactAPI.filter).toHaveBeenCalledWith(undefined, 'name', {
+ payload: [
+ {
+ attribute_key: 'email',
+ filter_operator: 'contains',
+ values: ['john'],
+ attribute_model: 'standard',
+ },
+ ],
+ });
+ });
+
it('handles empty search results', async () => {
ContactAPI.filter.mockResolvedValue({
data: { payload: [] },
@@ -310,6 +374,8 @@ describe('composeConversationHelper', () => {
const mockPayload = [
{
id: 1,
+ name: 'John Doe',
+ phone_number: '+1234567890',
contact_inboxes: [
{
inbox_id: 1,
@@ -332,6 +398,8 @@ describe('composeConversationHelper', () => {
expect(result).toEqual([
{
id: 1,
+ name: 'John Doe',
+ phoneNumber: '+1234567890',
contactInboxes: [
{
inboxId: 1,
diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
index b9824755e..95b368dae 100644
--- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
+++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue
@@ -95,7 +95,7 @@ onMounted(() => {
rounded-full
/>
-
+
{{ item.emoji }}
{{
item.label
diff --git a/app/javascript/dashboard/components-next/taginput/TagInput.vue b/app/javascript/dashboard/components-next/taginput/TagInput.vue
index 5338eadc6..63d757415 100644
--- a/app/javascript/dashboard/components-next/taginput/TagInput.vue
+++ b/app/javascript/dashboard/components-next/taginput/TagInput.vue
@@ -81,17 +81,19 @@ const filteredMenuItems = computed(() => {
item => !tags.value.includes(item.label)
);
- // Only show typed value as suggestion if:
+ // Show typed value as suggestion only if:
// 1. There's a value being typed
// 2. The value isn't already in the tags
- // 3. There are no menu items available
- const trimmedNewTag = newTag.value.trim();
- if (
+ // 3. Email validation passes (if type is email) and There are no menu items available
+ const trimmedNewTag = newTag.value?.trim();
+ const shouldShowTypedValue =
trimmedNewTag &&
!tags.value.includes(trimmedNewTag) &&
+ !props.isLoading &&
!availableMenuItems.length &&
- !props.isLoading
- ) {
+ (props.type === 'email' ? !isNewTagInValidType.value : true);
+
+ if (shouldShowTypedValue) {
return [
{
label: trimmedNewTag,
@@ -117,7 +119,7 @@ const emitDataOnAdd = emailValue => {
};
const addTag = async () => {
- const trimmedTag = newTag.value.trim();
+ const trimmedTag = newTag.value?.trim();
if (!trimmedTag) return;
if (props.mode === MODE.SINGLE && tags.value.length >= 1) {
@@ -185,7 +187,7 @@ watch(
watch(
() => newTag.value,
async newValue => {
- if (props.type === 'email' && newValue.trim()?.length > 2) {
+ if (props.type === 'email' && newValue?.trim()?.length > 2) {
await v$.value.$validate();
}
}
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
index 952f212ee..a38bbd20b 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
@@ -119,6 +119,13 @@ export default {
this.menuItem.toStateName === 'settings_applications'
);
},
+ isContactsDefaultRoute() {
+ return (
+ this.menuItem.toStateName === 'contacts_dashboard_index' &&
+ (this.$store.state.route.name === 'contacts_dashboard_index' ||
+ this.$store.state.route.name === 'contacts_edit')
+ );
+ },
isCurrentRoute() {
return this.$store.state.route.name.includes(this.menuItem.toStateName);
},
@@ -130,6 +137,7 @@ export default {
this.isAllConversations ||
this.isMentions ||
this.isUnattended ||
+ this.isContactsDefaultRoute ||
this.isCurrentRoute
) {
return 'bg-woot-25 dark:bg-slate-800 text-woot-500 dark:text-woot-500 hover:text-woot-500 dark:hover:text-woot-500 active-view';
@@ -242,7 +250,7 @@ export default {
-
+
{
switch (type) {
@@ -111,15 +124,17 @@ export const getInboxClassByType = (type, phoneNumber) => {
};
export const getInboxIconByType = (type, phoneNumber, variant = 'fill') => {
+ const iconMap =
+ variant === 'fill' ? INBOX_ICON_MAP_FILL : INBOX_ICON_MAP_LINE;
+ const defaultIcon =
+ variant === 'fill' ? DEFAULT_ICON_FILL : DEFAULT_ICON_LINE;
+
// Special case for Twilio (whatsapp and sms)
- if (type === INBOX_TYPES.TWILIO) {
- return phoneNumber?.startsWith('whatsapp')
- ? `i-ri-whatsapp-${variant}`
- : `i-ri-chat-1-${variant}`;
+ if (type === INBOX_TYPES.TWILIO && phoneNumber?.startsWith('whatsapp')) {
+ return iconMap[INBOX_TYPES.WHATSAPP];
}
- const baseIcon = INBOX_ICON_MAP[type] ?? DEFAULT_ICON;
- return `${baseIcon}-${variant}`;
+ return iconMap[type] ?? defaultIcon;
};
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {