Merge branch 'develop' into feat/chat-list-header

This commit is contained in:
Sivin Varghese
2024-12-10 11:30:34 +05:30
committed by GitHub
13 changed files with 200 additions and 46 deletions
@@ -14,7 +14,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search, :filter]
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
before_action :set_include_contact_inboxes, only: [:index, :search, :filter]
before_action :set_include_contact_inboxes, only: [:index, :search, :filter, :show, :update]
def index
@contacts_count = resolved_contacts.count
+8
View File
@@ -27,6 +27,14 @@ class ContactAPI extends ApiClient {
return axios.get(requestURL);
}
show(id) {
return axios.get(`${this.url}/${id}?include_contact_inboxes=false`);
}
update(id, data) {
return axios.patch(`${this.url}/${id}?include_contact_inboxes=false`, data);
}
getConversations(contactId) {
return axios.get(`${this.url}/${contactId}/conversations`);
}
@@ -70,6 +70,10 @@ const updateContact = async () => {
try {
const { customAttributes, ...basicContactData } = contactData.value;
await store.dispatch('contacts/update', basicContactData);
await store.dispatch(
'contacts/fetchContactableInbox',
props.selectedContact.id
);
useAlert(t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.ERROR_MESSAGE'));
@@ -153,7 +153,6 @@ watch(
activeContact,
() => {
if (activeContact.value && props.contactId) {
// Add null check for contactInboxes
const contactInboxes = activeContact.value?.contactInboxes || [];
selectedContact.value = {
...activeContact.value,
@@ -3,6 +3,15 @@ import { getInboxIconByType } from 'dashboard/helper/inbox';
import camelcaseKeys from 'camelcase-keys';
import ContactAPI from 'dashboard/api/contacts';
const CHANNEL_PRIORITY = {
'Channel::Email': 1,
'Channel::Whatsapp': 2,
'Channel::Sms': 3,
'Channel::TwilioSms': 4,
'Channel::WebWidget': 5,
'Channel::Api': 6,
};
export const generateLabelForContactableInboxesList = ({
name,
email,
@@ -21,27 +30,49 @@ export const generateLabelForContactableInboxesList = ({
return name;
};
const transformInbox = ({
name,
id,
email,
channelType,
phoneNumber,
...rest
}) => ({
id,
icon: getInboxIconByType(channelType, phoneNumber, 'line'),
label: generateLabelForContactableInboxesList({
name,
email,
channelType,
phoneNumber,
}),
action: 'inbox',
value: id,
name,
email,
phoneNumber,
channelType,
...rest,
});
export const compareInboxes = (a, b) => {
// Channels that have no priority defined should come at the end.
const priorityA = CHANNEL_PRIORITY[a.channelType] || 999;
const priorityB = CHANNEL_PRIORITY[b.channelType] || 999;
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
const nameA = a.name || '';
const nameB = b.name || '';
return nameA.localeCompare(nameB);
};
export const buildContactableInboxesList = contactInboxes => {
if (!contactInboxes) return [];
return contactInboxes.map(
({ name, id, email, channelType, phoneNumber, ...rest }) => ({
id,
icon: getInboxIconByType(channelType, phoneNumber, 'line'),
label: generateLabelForContactableInboxesList({
name,
email,
channelType,
phoneNumber,
}),
action: 'inbox',
value: id,
name,
email,
phoneNumber,
channelType,
...rest,
})
);
return contactInboxes.map(transformInbox).sort(compareInboxes);
};
export const getCapitalizedNameFromEmail = email => {
@@ -463,3 +463,74 @@ describe('composeConversationHelper', () => {
});
});
});
describe('compareInboxes', () => {
it('should sort inboxes by channel priority', () => {
const inboxes = [
{ channelType: 'Channel::Api', name: 'API Inbox' },
{ channelType: 'Channel::Email', name: 'Email Inbox' },
{ channelType: 'Channel::WebWidget', name: 'Widget' },
{ channelType: 'Channel::Whatsapp', name: 'WhatsApp' },
];
const sorted = [...inboxes].sort(helpers.compareInboxes);
expect(sorted[0].channelType).toBe('Channel::Email');
expect(sorted[1].channelType).toBe('Channel::Whatsapp');
expect(sorted[2].channelType).toBe('Channel::WebWidget');
expect(sorted[3].channelType).toBe('Channel::Api');
});
it('should sort SMS channels correctly', () => {
const inboxes = [
{ channelType: 'Channel::TwilioSms', name: 'Twilio' },
{ channelType: 'Channel::Sms', name: 'Regular SMS' },
];
const sorted = [...inboxes].sort(helpers.compareInboxes);
expect(sorted[0].channelType).toBe('Channel::Sms');
expect(sorted[1].channelType).toBe('Channel::TwilioSms');
});
it('should sort by name when channel types are same', () => {
const inboxes = [
{ channelType: 'Channel::Email', name: 'Support' },
{ channelType: 'Channel::Email', name: 'Marketing' },
{ channelType: 'Channel::Email', name: 'Billing' },
];
const sorted = [...inboxes].sort(helpers.compareInboxes);
expect(sorted.map(inbox => inbox.name)).toEqual([
'Billing',
'Marketing',
'Support',
]);
});
it('should put channels without priority at the end', () => {
const inboxes = [
{ channelType: 'Channel::Unknown', name: 'Unknown' },
{ channelType: 'Channel::Email', name: 'Email' },
{ channelType: 'Channel::LineChannel', name: 'Line' },
{ channelType: 'Channel::Whatsapp', name: 'WhatsApp' },
];
const sorted = [...inboxes].sort(helpers.compareInboxes);
expect(sorted.map(i => i.channelType)).toEqual([
'Channel::Email',
'Channel::Whatsapp',
'Channel::LineChannel',
'Channel::Unknown',
]);
});
it('should handle empty array', () => {
const inboxes = [];
const sorted = [...inboxes].sort(helpers.compareInboxes);
expect(sorted).toEqual([]);
});
});
@@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue';
import { computed, onMounted, nextTick } from 'vue';
import { useSidebarContext } from './provider';
import { useRoute, useRouter } from 'vue-router';
import Policy from 'dashboard/components/policy.vue';
@@ -113,6 +113,13 @@ const toggleTrigger = () => {
}
setExpandedItem(props.name);
};
onMounted(async () => {
await nextTick();
if (hasActiveChild.value) {
setExpandedItem(props.name);
}
});
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
@@ -62,7 +62,7 @@ const goToContactsList = () => {
const fetchActiveContact = async () => {
if (route.params.contactId) {
store.dispatch('contacts/show', { id: route.params.contactId });
await store.dispatch('contacts/show', { id: route.params.contactId });
await store.dispatch(
'contacts/fetchContactableInbox',
route.params.contactId
@@ -175,7 +175,10 @@ export default {
},
canLocktoSingleConversation() {
return (
this.isASmsInbox || this.isAWhatsAppChannel || this.isAFacebookInbox
this.isASmsInbox ||
this.isAWhatsAppChannel ||
this.isAFacebookInbox ||
this.isAPIInbox
);
},
inboxNameLabel() {
@@ -1,3 +1,3 @@
json.payload do
json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: true
json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: @include_contact_inboxes
end
@@ -1,3 +1,3 @@
json.payload do
json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: true
json.partial! 'api/v1/models/contact', formats: [:json], resource: @contact, with_contact_inboxes: @include_contact_inboxes
end
+1 -8
View File
@@ -3668,11 +3668,6 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -9073,8 +9068,6 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.7: {}
nanoid@3.3.8: {}
nanoid@5.0.8: {}
@@ -9564,7 +9557,7 @@ snapshots:
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
nanoid: 3.3.8
picocolors: 1.1.0
source-map-js: 1.2.1
+50 -12
View File
@@ -3,39 +3,77 @@ require 'rails_helper'
describe ConversationBuilder do
let(:account) { create(:account) }
let!(:sms_channel) { create(:channel_sms, account: account) }
let!(:api_channel) { create(:channel_api, account: account) }
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
let(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: sms_inbox) }
let(:contact_sms_inbox) { create(:contact_inbox, contact: contact, inbox: sms_inbox) }
let(:contact_api_inbox) { create(:contact_inbox, contact: contact, inbox: api_inbox) }
describe '#perform' do
it 'creates conversation' do
it 'creates sms conversation' do
conversation = described_class.new(
contact_inbox: contact_inbox,
contact_inbox: contact_sms_inbox,
params: {}
).perform
expect(conversation.contact_inbox_id).to eq(contact_inbox.id)
expect(conversation.contact_inbox_id).to eq(contact_sms_inbox.id)
end
context 'when lock_to_single_conversation is true for inbox' do
it 'creates api conversation' do
conversation = described_class.new(
contact_inbox: contact_api_inbox,
params: {}
).perform
expect(conversation.contact_inbox_id).to eq(contact_api_inbox.id)
end
context 'when lock_to_single_conversation is true for sms inbox' do
before do
sms_inbox.update!(lock_to_single_conversation: true)
end
it 'creates conversation when existing conversation is not present' do
it 'creates sms conversation when existing conversation is not present' do
conversation = described_class.new(
contact_inbox: contact_inbox,
contact_inbox: contact_sms_inbox,
params: {}
).perform
expect(conversation.contact_inbox_id).to eq(contact_inbox.id)
expect(conversation.contact_inbox_id).to eq(contact_sms_inbox.id)
end
it 'returns last from existing conversations when existing conversation is not present' do
create(:conversation, contact_inbox: contact_inbox)
existing_conversation = create(:conversation, contact_inbox: contact_inbox)
it 'returns last from existing sms conversations when existing conversation is not present' do
create(:conversation, contact_inbox: contact_sms_inbox)
existing_conversation = create(:conversation, contact_inbox: contact_sms_inbox)
conversation = described_class.new(
contact_inbox: contact_inbox,
contact_inbox: contact_sms_inbox,
params: {}
).perform
expect(conversation.id).to eq(existing_conversation.id)
end
end
context 'when lock_to_single_conversation is true for api inbox' do
before do
api_inbox.update!(lock_to_single_conversation: true)
end
it 'creates conversation when existing api conversation is not present' do
conversation = described_class.new(
contact_inbox: contact_api_inbox,
params: {}
).perform
expect(conversation.contact_inbox_id).to eq(contact_api_inbox.id)
end
it 'returns last from existing api conversations when existing conversation is not present' do
create(:conversation, contact_inbox: contact_api_inbox)
existing_conversation = create(:conversation, contact_inbox: contact_api_inbox)
conversation = described_class.new(
contact_inbox: contact_api_inbox,
params: {}
).perform