diff --git a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue index 9a49fc39a..93b0baa3d 100644 --- a/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue +++ b/app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue @@ -13,6 +13,7 @@ import { createNewContact, fetchContactableInboxes, processContactableInboxes, + mergeInboxDetails, } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper'; import ComposeNewConversationForm from 'dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue'; @@ -47,6 +48,7 @@ const currentUser = useMapGetter('getCurrentUser'); const globalConfig = useMapGetter('globalConfig/get'); const uiFlags = useMapGetter('contactConversations/getUIFlags'); const messageSignature = useMapGetter('getMessageSignature'); +const inboxesList = useMapGetter('inboxes/getInboxes'); const sendWithSignature = computed(() => fetchSignatureFlagFromUISettings(targetInbox.value?.channelType) @@ -104,7 +106,12 @@ const handleSelectedContact = async ({ value, action, ...rest }) => { isFetchingInboxes.value = true; try { const contactableInboxes = await fetchContactableInboxes(contact.id); - selectedContact.value.contactInboxes = contactableInboxes; + // Merge the processed contactableInboxes with the inboxesList + selectedContact.value.contactInboxes = mergeInboxDetails( + contactableInboxes, + inboxesList.value + ); + isFetchingInboxes.value = false; } catch (error) { isFetchingInboxes.value = false; @@ -162,9 +169,12 @@ watch( () => { if (activeContact.value && props.contactId) { const contactInboxes = activeContact.value?.contactInboxes || []; + // First process the contactable inboxes to get the right structure + const processedInboxes = processContactableInboxes(contactInboxes); + // Then Merge processedInboxes with the inboxes list selectedContact.value = { ...activeContact.value, - contactInboxes: processContactableInboxes(contactInboxes), + contactInboxes: mergeInboxDetails(processedInboxes, inboxesList.value), }; } }, diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js index c5d3c6264..57e819245 100644 --- a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js +++ b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js @@ -87,6 +87,21 @@ export const processContactableInboxes = inboxes => { })); }; +export const mergeInboxDetails = (inboxesData, inboxesList = []) => { + if (!inboxesData || !inboxesData.length) { + return []; + } + + return inboxesData.map(inboxData => { + const matchingInbox = + inboxesList.find(inbox => inbox.id === inboxData.id) || {}; + return { + ...camelcaseKeys(matchingInbox, { deep: true }), + ...inboxData, + }; + }); +}; + export const prepareAttachmentPayload = ( attachedFiles, directUploadsEnabled 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 9307e657c..3de1fad0d 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 @@ -110,6 +110,153 @@ describe('composeConversationHelper', () => { }); }); + describe('mergeInboxDetails', () => { + it('returns empty array if inboxesData is empty or null', () => { + expect(helpers.mergeInboxDetails(null)).toEqual([]); + expect(helpers.mergeInboxDetails([])).toEqual([]); + expect(helpers.mergeInboxDetails(undefined)).toEqual([]); + }); + + it('merges inbox data with matching inboxes from the list', () => { + const inboxesData = [ + { id: 1, sourceId: 'source1' }, + { id: 2, sourceId: 'source2' }, + ]; + + const inboxesList = [ + { + id: 1, + name: 'Inbox 1', + channel_type: 'Channel::Email', + channel_id: 10, + phone_number: null, + }, + { + id: 2, + name: 'Inbox 2', + channel_type: 'Channel::Whatsapp', + channel_id: 20, + phone_number: '+1234567890', + }, + { + id: 3, + name: 'Inbox 3', + channel_type: 'Channel::Api', + channel_id: 30, + phone_number: null, + }, + ]; + + const result = helpers.mergeInboxDetails(inboxesData, inboxesList); + + expect(result.length).toBe(2); + expect(result[0]).toMatchObject({ + id: 1, + sourceId: 'source1', + name: 'Inbox 1', + channelType: 'Channel::Email', + channelId: 10, + phoneNumber: null, + }); + + expect(result[1]).toMatchObject({ + id: 2, + sourceId: 'source2', + name: 'Inbox 2', + channelType: 'Channel::Whatsapp', + channelId: 20, + phoneNumber: '+1234567890', + }); + }); + + it('handles inboxes not found in the list', () => { + const inboxesData = [ + { id: 1, sourceId: 'source1' }, + { id: 99, sourceId: 'source99' }, // This doesn't exist in inboxesList + ]; + + const inboxesList = [ + { + id: 1, + name: 'Inbox 1', + channel_type: 'Channel::Email', + }, + ]; + + const result = helpers.mergeInboxDetails(inboxesData, inboxesList); + + expect(result.length).toBe(2); + + expect(result[0]).toMatchObject({ + id: 1, + sourceId: 'source1', + name: 'Inbox 1', + channelType: 'Channel::Email', + }); + + expect(result[1]).toMatchObject({ + id: 99, + sourceId: 'source99', + }); + + expect(result[1].name).toBeUndefined(); + expect(result[1].channelType).toBeUndefined(); + }); + + it('camelcases properties from inboxesList', () => { + const inboxesData = [{ id: 1, sourceId: 'source1' }]; + + const inboxesList = [ + { + id: 1, + name: 'Inbox 1', + channel_type: 'Channel::Email', + avatar_url: 'https://example.com/avatar.png', + working_hours: [ + { + day_of_week: 1, + closed_all_day: false, + }, + ], + }, + ]; + + const result = helpers.mergeInboxDetails(inboxesData, inboxesList); + + expect(result[0]).toMatchObject({ + id: 1, + sourceId: 'source1', + name: 'Inbox 1', + channelType: 'Channel::Email', + avatarUrl: 'https://example.com/avatar.png', + }); + + expect(result[0].workingHours[0]).toMatchObject({ + dayOfWeek: 1, + closedAllDay: false, + }); + }); + + it('preserves original properties when they conflict with inboxesList', () => { + const inboxesData = [ + { id: 1, sourceId: 'source1', name: 'Original Name' }, + ]; + + const inboxesList = [ + { + id: 1, + name: 'List Name', + channel_type: 'Channel::Email', + }, + ]; + + const result = helpers.mergeInboxDetails(inboxesData, inboxesList); + + expect(result[0].name).toBe('Original Name'); + expect(result[0].channelType).toBe('Channel::Email'); + }); + }); + describe('prepareAttachmentPayload', () => { it('prepares direct upload files', () => { const files = [{ blobSignedId: 'signed1' }]; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue index 47517798b..9b5614e8a 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -105,6 +105,7 @@ export default { currentUser: 'getCurrentUser', globalConfig: 'globalConfig/get', messageSignature: 'getMessageSignature', + inboxesList: 'inboxes/getInboxes', }), sendWithSignature() { return this.fetchSignatureFlagFromUISettings(this.channelType); @@ -139,13 +140,29 @@ export default { selectedInbox: { get() { const inboxList = this.contact.contact_inboxes || []; - return ( - inboxList.find(inbox => { - return inbox.inbox?.id && inbox.inbox?.id === this.targetInbox?.id; - }) || { - inbox: {}, - } + const selectedContactInbox = inboxList.find( + inbox => inbox.inbox?.id && inbox.inbox?.id === this.targetInbox?.id ); + + if (!selectedContactInbox) { + return { inbox: {} }; + } + + // Find the matching inbox from the inboxesList + const matchingInbox = + this.inboxesList.find( + item => item.id === selectedContactInbox.inbox?.id + ) || {}; + + // The entire inbox payload is not available in this object, so we need to patch it from the store + return { + ...selectedContactInbox, + inbox: { + ...matchingInbox, + ...selectedContactInbox.inbox, + sourceId: selectedContactInbox.source_id || matchingInbox.sourceId, + }, + }; }, set(value) { this.targetInbox = value.inbox; @@ -165,12 +182,22 @@ export default { ? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP') : this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP'); }, + inboxes() { const inboxList = this.contact.contact_inboxes || []; - return inboxList.map(inbox => ({ - ...inbox.inbox, - sourceId: inbox.source_id, - })); + if (!inboxList.length) return []; + + return inboxList.map(inbox => { + const matchingInbox = + this.inboxesList.find(item => item.id === inbox.inbox?.id) || {}; + + // Create merged object with a clear property order + return { + ...matchingInbox, + ...inbox.inbox, + sourceId: inbox.source_id, + }; + }); }, isAnEmailInbox() { return ( diff --git a/app/views/api/v1/models/_inbox_slim.json.jbuilder b/app/views/api/v1/models/_inbox_slim.json.jbuilder index 67bc7e843..c47aabf30 100644 --- a/app/views/api/v1/models/_inbox_slim.json.jbuilder +++ b/app/views/api/v1/models/_inbox_slim.json.jbuilder @@ -4,10 +4,3 @@ json.channel_id resource.channel_id json.name resource.name json.channel_type resource.channel_type json.provider resource.channel.try(:provider) - -# Fix me: this is for the new conversation modal to work, -# Potentially refactor this later. -json.email resource.channel.try(:email) if resource.email? -json.phone_number resource.channel.try(:phone_number) -json.medium resource.channel.try(:medium) if resource.twilio? -json.message_templates resource.channel.try(:message_templates) if resource.whatsapp?