fix: New compose conversation form (#10548)

This commit is contained in:
Sivin Varghese
2024-12-06 15:40:06 +05:30
committed by GitHub
parent afb3e3e649
commit 1b430ffae2
9 changed files with 150 additions and 35 deletions

View File

@@ -1,6 +1,7 @@
<script setup>
import { computed, useSlots } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import Button from 'dashboard/components-next/button/Button.vue';
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
@@ -21,6 +22,9 @@ const emit = defineEmits(['goToContactsList']);
const { t } = useI18n();
const slots = useSlots();
const route = useRoute();
const contactId = computed(() => route.params.contactId);
const selectedContactName = computed(() => {
return props.selectedContact?.name;
@@ -60,7 +64,7 @@ const handleBreadcrumbClick = () => {
:items="breadcrumbItems"
@click="handleBreadcrumbClick"
/>
<ComposeConversation>
<ComposeConversation :contact-id="contactId">
<template #trigger="{ toggle }">
<Button :label="buttonLabel" size="sm" @click="toggle" />
</template>

View File

@@ -2,7 +2,7 @@
import { ref, computed, onMounted, watch } from 'vue';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { vOnClickOutside } from '@vueuse/components';
import { useAlert } from 'dashboard/composables';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
import { debounce } from '@chatwoot/utils';
@@ -21,9 +21,12 @@ const props = defineProps({
type: String,
default: 'left',
},
contactId: {
type: String,
default: null,
},
});
const route = useRoute();
const store = useStore();
const { t } = useI18n();
@@ -44,8 +47,8 @@ const uiFlags = useMapGetter('contactConversations/getUIFlags');
const directUploadsEnabled = computed(
() => globalConfig.value.directUploadsEnabled
);
const contactId = computed(() => route.params.contactId || null);
const activeContact = computed(() => contactById.value(contactId.value));
const activeContact = computed(() => contactById.value(props.contactId));
const composePopoverClass = computed(() => {
return props.alignPosition === 'right'
@@ -149,7 +152,7 @@ const toggle = () => {
watch(
activeContact,
() => {
if (activeContact.value && contactId.value) {
if (activeContact.value && props.contactId) {
// Add null check for contactInboxes
const contactInboxes = activeContact.value?.contactInboxes || [];
selectedContact.value = {
@@ -177,7 +180,10 @@ useKeyboardEvents(keyboardEvents);
</script>
<template>
<div class="relative z-40">
<div
v-on-click-outside="() => (showComposeNewConversation = false)"
class="relative z-40"
>
<slot
name="trigger"
:is-open="showComposeNewConversation"

View File

@@ -65,7 +65,14 @@ const contactsList = computed(() => {
});
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(() => {

View File

@@ -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 => {

View File

@@ -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,

View File

@@ -95,7 +95,7 @@ onMounted(() => {
rounded-full
/>
</slot>
<Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0" />
<Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0 size-3.5" />
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
<span v-if="item.label" class="min-w-0 text-sm truncate">{{
item.label

View File

@@ -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();
}
}