Merge branch 'develop' into feat/chat-list-header
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+50
-19
@@ -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 => {
|
||||
|
||||
+71
@@ -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
|
||||
|
||||
Generated
+1
-8
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user