feat: Code clean up
This commit is contained in:
@@ -169,7 +169,7 @@ watch(
|
||||
@apply m-0 !important;
|
||||
|
||||
&::before {
|
||||
@apply text-n-slate-10 dark:text-n-slate-10 !important;
|
||||
@apply text-n-slate-10 dark:text-n-slate-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+118
-163
@@ -4,14 +4,22 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, requiredIf } from '@vuelidate/validators';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
import {
|
||||
buildContactableInboxesList,
|
||||
createNewContact,
|
||||
fetchContactableInboxes,
|
||||
prepareNewMessagePayload,
|
||||
prepareWhatsAppMessagePayload,
|
||||
processContactableInboxes,
|
||||
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper.js';
|
||||
|
||||
import ContactAPI from 'dashboard/api/contacts';
|
||||
import ContactSelector from './components/ContactSelector.vue';
|
||||
import InboxSelector from './components/InboxSelector.vue';
|
||||
import EmailOptions from './components/EmailOptions.vue';
|
||||
@@ -46,7 +54,14 @@ const isCreatingContact = ref(false);
|
||||
|
||||
const contactById = useMapGetter('contacts/getContactById');
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
const uiFlags = useMapGetter('contactConversations/getUIFlags');
|
||||
const activeContact = computed(() => contactById.value(props.contactId));
|
||||
const directUploadsEnabled = computed(
|
||||
() => globalConfig.value.directUploadsEnabled
|
||||
);
|
||||
|
||||
const isCreating = computed(() => uiFlags.value.isCreating);
|
||||
|
||||
const selectedContact = ref(null);
|
||||
const targetInbox = ref(null);
|
||||
@@ -56,117 +71,66 @@ const state = reactive({
|
||||
subject: '',
|
||||
ccEmails: '',
|
||||
bccEmails: '',
|
||||
attachedFiles: [],
|
||||
});
|
||||
|
||||
const showBccInput = ref(false);
|
||||
|
||||
const isEmailInbox = computed(() => {
|
||||
return targetInbox.value?.channelType === INBOX_TYPES.EMAIL;
|
||||
});
|
||||
const inboxTypes = computed(() => ({
|
||||
isEmail: targetInbox.value?.channelType === INBOX_TYPES.EMAIL,
|
||||
isTwilio: targetInbox.value?.channelType === INBOX_TYPES.TWILIO,
|
||||
isWhatsapp: targetInbox.value?.channelType === INBOX_TYPES.WHATSAPP,
|
||||
isWebWidget: targetInbox.value?.channelType === INBOX_TYPES.WEB,
|
||||
isApi: targetInbox.value?.channelType === INBOX_TYPES.API,
|
||||
isEmailOrWebWidget:
|
||||
targetInbox.value?.channelType === INBOX_TYPES.EMAIL ||
|
||||
targetInbox.value?.channelType === INBOX_TYPES.WEB,
|
||||
}));
|
||||
|
||||
const isTwilioInbox = computed(() => {
|
||||
return targetInbox.value?.channelType === INBOX_TYPES.TWILIO;
|
||||
});
|
||||
const whatsappMessageTemplates = computed(() =>
|
||||
Object.keys(targetInbox.value?.messageTemplates || {}).length
|
||||
? targetInbox.value.messageTemplates
|
||||
: []
|
||||
);
|
||||
|
||||
const isWhatsappInbox = computed(() => {
|
||||
return targetInbox.value?.channelType === INBOX_TYPES.WHATSAPP;
|
||||
});
|
||||
const inboxChannelType = computed(() => targetInbox.value?.channelType || '');
|
||||
|
||||
const isWebWidgetInbox = computed(() => {
|
||||
return targetInbox.value?.channelType === INBOX_TYPES.WEB;
|
||||
});
|
||||
const validationRules = computed(() => ({
|
||||
selectedContact: { required },
|
||||
targetInbox: { required },
|
||||
message: { required: requiredIf(!inboxTypes.value.isWhatsapp) },
|
||||
subject: { required: requiredIf(inboxTypes.value.isEmail) },
|
||||
}));
|
||||
|
||||
const isApiInbox = computed(() => {
|
||||
return targetInbox.value?.channelType === INBOX_TYPES.API;
|
||||
});
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const isEmailOrWebWidgetInbox = computed(() => {
|
||||
return isEmailInbox.value || isWebWidgetInbox.value;
|
||||
});
|
||||
|
||||
const whatsappMessageTemplates = computed(() => {
|
||||
return targetInbox.value?.messageTemplates;
|
||||
});
|
||||
|
||||
const inboxChannelType = computed(() => {
|
||||
return targetInbox.value?.channelType || '';
|
||||
});
|
||||
const validationStates = computed(() => ({
|
||||
isContactInvalid:
|
||||
v$.value.selectedContact.$dirty && v$.value.selectedContact.$invalid,
|
||||
isInboxInvalid: v$.value.targetInbox.$dirty && v$.value.targetInbox.$invalid,
|
||||
isSubjectInvalid: v$.value.subject.$dirty && v$.value.subject.$invalid,
|
||||
isMessageInvalid: v$.value.message.$dirty && v$.value.message.$invalid,
|
||||
}));
|
||||
|
||||
const newMessagePayload = () => {
|
||||
const payload = {
|
||||
inboxId: targetInbox.value.id,
|
||||
sourceId: targetInbox.value.sourceId,
|
||||
contactId: Number(selectedContact.value.id),
|
||||
message: { content: state.message },
|
||||
assigneeId: currentUser.value.id,
|
||||
};
|
||||
|
||||
// if (this.attachedFiles && this.attachedFiles.length) {
|
||||
// payload.files = [];
|
||||
// setAttachmentPayload(payload);
|
||||
// }
|
||||
|
||||
if (state.subject) {
|
||||
payload.mailSubject = state.subject;
|
||||
}
|
||||
|
||||
if (state.ccEmails) {
|
||||
payload.message.cc_emails = state.ccEmails;
|
||||
}
|
||||
|
||||
if (state.bccEmails) {
|
||||
payload.message.bcc_emails = state.bccEmails;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const generateLabelForContactableInboxesList = ({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}) => {
|
||||
if (channelType === INBOX_TYPES.EMAIL) {
|
||||
return `${name} (${email})`;
|
||||
}
|
||||
if (
|
||||
channelType === INBOX_TYPES.TWILIO ||
|
||||
channelType === INBOX_TYPES.WHATSAPP
|
||||
) {
|
||||
return `${name} (${phoneNumber})`;
|
||||
}
|
||||
if (channelType === INBOX_TYPES.API) {
|
||||
return `${name} (API)`;
|
||||
}
|
||||
return name;
|
||||
const { message, subject, ccEmails, bccEmails, attachedFiles } = state;
|
||||
return prepareNewMessagePayload({
|
||||
targetInbox: targetInbox.value,
|
||||
selectedContact: selectedContact.value,
|
||||
message,
|
||||
subject,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
currentUser: currentUser.value,
|
||||
attachedFiles,
|
||||
directUploadsEnabled: directUploadsEnabled.value,
|
||||
});
|
||||
};
|
||||
|
||||
const contactableInboxesList = computed(() => {
|
||||
return selectedContact.value?.contactInboxes?.map(
|
||||
({ name, id, email, channelType, phoneNumber, ...rest }) => ({
|
||||
id,
|
||||
label: generateLabelForContactableInboxesList({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}),
|
||||
action: 'inbox',
|
||||
value: id,
|
||||
name,
|
||||
email,
|
||||
phoneNumber,
|
||||
channelType,
|
||||
...rest,
|
||||
})
|
||||
);
|
||||
return buildContactableInboxesList(selectedContact.value?.contactInboxes);
|
||||
});
|
||||
|
||||
const getCapitalizedNameFromEmail = email => {
|
||||
const name = email.match(/^([^@]*)@/)?.[1] || email.split('@')[0];
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
};
|
||||
|
||||
const handleContactSearch = value => {
|
||||
emit('searchContacts', value);
|
||||
};
|
||||
@@ -197,56 +161,31 @@ const searchBccEmails = value => {
|
||||
|
||||
const setSelectedContact = async ({ value, action, ...rest }) => {
|
||||
try {
|
||||
v$.value.$reset();
|
||||
let contact;
|
||||
|
||||
if (action === 'create') {
|
||||
isCreatingContact.value = true;
|
||||
const payload = {
|
||||
name: getCapitalizedNameFromEmail(value),
|
||||
email: value,
|
||||
};
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
payload: { contact: newContact },
|
||||
},
|
||||
} = await ContactAPI.create(payload);
|
||||
contact = camelcaseKeys(newContact, {
|
||||
deep: true,
|
||||
});
|
||||
contact = await createNewContact(value);
|
||||
isCreatingContact.value = false;
|
||||
} catch (error) {
|
||||
isCreatingContact.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedContact.value = contact;
|
||||
isCreatingContact.value = false;
|
||||
} else {
|
||||
contact = rest;
|
||||
selectedContact.value = contact;
|
||||
}
|
||||
|
||||
selectedContact.value = contact;
|
||||
showContactsDropdown.value = false;
|
||||
|
||||
// Only proceed with fetching inboxes if we have a contact
|
||||
if (contact?.id) {
|
||||
const {
|
||||
data: { payload: inboxes = [] },
|
||||
} = await ContactAPI.getContactableInboxes(contact.id);
|
||||
|
||||
const contactableInboxes = inboxes.map(inbox => ({
|
||||
...inbox.inbox,
|
||||
sourceId: inbox.source_id,
|
||||
}));
|
||||
|
||||
selectedContact.value.contactInboxes = camelcaseKeys(contactableInboxes, {
|
||||
deep: true,
|
||||
});
|
||||
const contactableInboxes = await fetchContactableInboxes(contact.id);
|
||||
selectedContact.value.contactInboxes = contactableInboxes;
|
||||
showInboxesDropdown.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Error in setSelectedContact:', error);
|
||||
// Reset states in case of error
|
||||
isCreatingContact.value = false;
|
||||
showContactsDropdown.value = false;
|
||||
@@ -254,12 +193,18 @@ const setSelectedContact = async ({ value, action, ...rest }) => {
|
||||
};
|
||||
|
||||
const handleInboxAction = ({ value, action, ...rest }) => {
|
||||
v$.value.$reset();
|
||||
targetInbox.value = {
|
||||
...rest,
|
||||
};
|
||||
showInboxesDropdown.value = false;
|
||||
};
|
||||
|
||||
const removeTargetInbox = value => {
|
||||
v$.value.$reset();
|
||||
targetInbox.value = value;
|
||||
};
|
||||
|
||||
const clearSelectedContact = () => {
|
||||
selectedContact.value = null;
|
||||
targetInbox.value = null;
|
||||
@@ -278,10 +223,14 @@ const handleRemoveSignature = signature => {
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
state.message = '';
|
||||
state.subject = '';
|
||||
state.ccEmails = '';
|
||||
state.bccEmails = '';
|
||||
Object.assign(state, {
|
||||
message: '',
|
||||
subject: '',
|
||||
ccEmails: '',
|
||||
bccEmails: '',
|
||||
attachedFiles: [],
|
||||
});
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const createConversation = async ({ payload, isFromWhatsApp }) => {
|
||||
@@ -298,15 +247,18 @@ const createConversation = async ({ payload, isFromWhatsApp }) => {
|
||||
emit('success');
|
||||
useAlert(t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
|
||||
} catch (error) {
|
||||
if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
useAlert(
|
||||
error instanceof ExceptionWithMessage
|
||||
? error.data
|
||||
: t('NEW_CONVERSATION.FORM.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const isValid = await v$.value.$validate();
|
||||
if (!isValid) return;
|
||||
|
||||
await createConversation({
|
||||
payload: newMessagePayload(),
|
||||
isFromWhatsApp: false,
|
||||
@@ -315,20 +267,18 @@ const handleSendMessage = async () => {
|
||||
clearForm();
|
||||
};
|
||||
|
||||
const prepareWhatsAppMessagePayload = ({ message, templateParams }) => {
|
||||
const payload = {
|
||||
inboxId: targetInbox.value.id,
|
||||
sourceId: targetInbox.value.sourceId,
|
||||
contactId: selectedContact.value.id,
|
||||
message: { content: message, template_params: templateParams },
|
||||
assigneeId: currentUser.value.id,
|
||||
};
|
||||
return payload;
|
||||
};
|
||||
|
||||
const handleSendWhatsappMessage = async payload => {
|
||||
const whatsappPayload = prepareWhatsAppMessagePayload(payload);
|
||||
await createConversation({ payload: whatsappPayload, isFromWhatsApp: true });
|
||||
const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
const whatsappMessagePayload = prepareWhatsAppMessagePayload({
|
||||
targetInbox: targetInbox.value,
|
||||
selectedContact: selectedContact.value,
|
||||
message,
|
||||
templateParams,
|
||||
currentUser: currentUser.value,
|
||||
});
|
||||
await createConversation({
|
||||
payload: whatsappMessagePayload,
|
||||
isFromWhatsApp: true,
|
||||
});
|
||||
emit('discard');
|
||||
};
|
||||
|
||||
@@ -338,10 +288,9 @@ watch(
|
||||
if (activeContact.value && props.contactId) {
|
||||
selectedContact.value = {
|
||||
...activeContact.value,
|
||||
contactInboxes: activeContact.value?.contactInboxes.map(inbox => ({
|
||||
...inbox.inbox,
|
||||
sourceId: inbox.sourceId,
|
||||
})),
|
||||
contactInboxes: processContactableInboxes(
|
||||
activeContact.value?.contactInboxes
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -363,6 +312,7 @@ watch(
|
||||
:contact-id="contactId"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
:show-inboxes-dropdown="showInboxesDropdown"
|
||||
:has-errors="validationStates.isContactInvalid"
|
||||
@search-contacts="handleContactSearch"
|
||||
@set-selected-contact="setSelectedContact"
|
||||
@clear-selected-contact="clearSelectedContact"
|
||||
@@ -374,13 +324,14 @@ watch(
|
||||
:selected-contact="selectedContact"
|
||||
:show-inboxes-dropdown="showInboxesDropdown"
|
||||
:contactable-inboxes-list="contactableInboxesList"
|
||||
@update-inbox="targetInbox = $event"
|
||||
:has-errors="validationStates.isInboxInvalid"
|
||||
@update-inbox="removeTargetInbox"
|
||||
@toggle-dropdown="showInboxesDropdown = $event"
|
||||
@handle-inbox-action="handleInboxAction"
|
||||
/>
|
||||
|
||||
<EmailOptions
|
||||
v-if="isEmailInbox"
|
||||
v-if="inboxTypes.isEmail"
|
||||
v-model:cc-emails="state.ccEmails"
|
||||
v-model:bcc-emails="state.bccEmails"
|
||||
v-model:subject="state.subject"
|
||||
@@ -389,6 +340,7 @@ watch(
|
||||
:show-bcc-emails-dropdown="showBccEmailsDropdown"
|
||||
:show-bcc-input="showBccInput"
|
||||
:is-loading="isLoading"
|
||||
:has-errors="validationStates.isSubjectInvalid"
|
||||
@search-cc-emails="searchCcEmails"
|
||||
@search-bcc-emails="searchBccEmails"
|
||||
@toggle-bcc="toggleBccInput"
|
||||
@@ -396,19 +348,22 @@ watch(
|
||||
/>
|
||||
|
||||
<MessageEditor
|
||||
v-if="!isWhatsappInbox"
|
||||
v-if="!inboxTypes.isWhatsapp"
|
||||
v-model="state.message"
|
||||
:is-email-or-web-widget-inbox="isEmailOrWebWidgetInbox"
|
||||
:is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget"
|
||||
:has-errors="validationStates.isMessageInvalid"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ActionButtons
|
||||
:is-whatsapp-inbox="isWhatsappInbox"
|
||||
:is-email-or-web-widget-inbox="isEmailOrWebWidgetInbox"
|
||||
:is-twilio-inbox="isTwilioInbox"
|
||||
:is-api-inbox="isApiInbox"
|
||||
:is-whatsapp-inbox="inboxTypes.isWhatsapp"
|
||||
:is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget"
|
||||
:is-twilio-inbox="inboxTypes.isTwilio"
|
||||
:is-api-inbox="inboxTypes.isApi"
|
||||
:message-templates="whatsappMessageTemplates"
|
||||
:channel-type="inboxChannelType"
|
||||
:is-loading="isCreating"
|
||||
:disable-send-button="isCreating"
|
||||
@discard="$emit('discard')"
|
||||
@send-message="handleSendMessage"
|
||||
@send-whatsapp-message="handleSendWhatsappMessage"
|
||||
|
||||
+12
-10
@@ -15,22 +15,22 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
// isTwilioInbox: {
|
||||
// type: Boolean,
|
||||
// required: true,
|
||||
// },
|
||||
// isApiInbox: {
|
||||
// type: Boolean,
|
||||
// required: true,
|
||||
// },
|
||||
messageTemplates: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
channelType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableSendButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -141,6 +141,8 @@ const onClickInsertEmoji = emoji => {
|
||||
label="Send (↵)"
|
||||
size="sm"
|
||||
class="!text-xs font-medium"
|
||||
:disabled="isLoading || disableSendButton"
|
||||
:is-loading="isLoading"
|
||||
@click="emit('sendMessage')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import TagInput from 'dashboard/components-next/taginput/TagInput.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
@@ -36,6 +37,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
hasErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -96,6 +101,11 @@ const selectedContactLabel = computed(() => {
|
||||
:disabled="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
type="email"
|
||||
class="flex-1 min-h-7"
|
||||
:class="
|
||||
hasErrors
|
||||
? '[&_input]:placeholder:!text-n-ruby-9 [&_input]:dark:placeholder:!text-n-ruby-9'
|
||||
: ''
|
||||
"
|
||||
@focus="emit('updateDropdown', 'contacts', true)"
|
||||
@input="emit('searchContacts', $event)"
|
||||
@on-click-outside="emit('updateDropdown', 'contacts', false)"
|
||||
|
||||
+11
-2
@@ -32,11 +32,15 @@ const props = defineProps({
|
||||
},
|
||||
showBccInput: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
hasErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -88,6 +92,11 @@ const handleBccUpdate = value => {
|
||||
placeholder="Enter your email subject here"
|
||||
label="Subject :"
|
||||
focus-on-mount
|
||||
:custom-input-class="
|
||||
hasErrors
|
||||
? 'placeholder:!text-n-ruby-9 dark:placeholder:!text-n-ruby-9'
|
||||
: ''
|
||||
"
|
||||
@update:model-value="emit('update:subject', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+6
-19
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { generateLabelForContactableInboxesList } from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper.js';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
@@ -21,6 +22,10 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
hasErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -29,24 +34,6 @@ const emit = defineEmits([
|
||||
'handleInboxAction',
|
||||
]);
|
||||
|
||||
const generateLabelForContactableInboxesList = ({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}) => {
|
||||
if (channelType === 'EMAIL') {
|
||||
return `${name} (${email})`;
|
||||
}
|
||||
if (channelType === 'TWILIO' || channelType === 'WHATSAPP') {
|
||||
return `${name} (${phoneNumber})`;
|
||||
}
|
||||
if (channelType === 'API') {
|
||||
return `${name} (API)`;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const targetInboxLabel = computed(() => {
|
||||
return generateLabelForContactableInboxesList(props.targetInbox);
|
||||
});
|
||||
@@ -84,7 +71,7 @@ const targetInboxLabel = computed(() => {
|
||||
label="Show inboxes"
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="slate"
|
||||
:color="hasErrors ? 'ruby' : 'slate'"
|
||||
:disabled="!selectedContact"
|
||||
class="hover:!no-underline"
|
||||
@click="emit('toggleDropdown', !showInboxesDropdown)"
|
||||
|
||||
@@ -11,6 +11,10 @@ defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
hasErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@@ -22,6 +26,11 @@ const emit = defineEmits(['update:modelValue']);
|
||||
:model-value="modelValue"
|
||||
placeholder="Write your message here..."
|
||||
class="[&>div]:!border-transparent [&>div]:px-4 [&>div]:py-4 [&>div]:!bg-transparent h-full [&_.ProseMirror-woot-style]:!max-h-[200px]"
|
||||
:class="
|
||||
hasErrors
|
||||
? '[&_.empty-node]:before:!text-n-ruby-9 [&_.empty-node]:dark:before:!text-n-ruby-9'
|
||||
: ''
|
||||
"
|
||||
:show-character-count="false"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
@@ -32,6 +41,11 @@ const emit = defineEmits(['update:modelValue']);
|
||||
placeholder="Write your message here..."
|
||||
class="!px-0 [&>div]:!px-4 [&>div]:!border-transparent [&>div]:!bg-transparent"
|
||||
auto-height
|
||||
:custom-text-area-class="
|
||||
hasErrors
|
||||
? 'placeholder:!text-n-ruby-9 dark:placeholder:!text-n-ruby-9'
|
||||
: ''
|
||||
"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import ContactAPI from 'dashboard/api/contacts';
|
||||
|
||||
export const convertChannelTypeToLabel = channelType => {
|
||||
const [, type] = channelType.split('::');
|
||||
return type ? type.charAt(0).toUpperCase() + type.slice(1) : channelType;
|
||||
};
|
||||
|
||||
export const generateLabelForContactableInboxesList = ({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}) => {
|
||||
if (channelType === INBOX_TYPES.EMAIL) {
|
||||
return `${name} (${email})`;
|
||||
}
|
||||
if (
|
||||
channelType === INBOX_TYPES.TWILIO ||
|
||||
channelType === INBOX_TYPES.WHATSAPP
|
||||
) {
|
||||
return `${name} (${phoneNumber})`;
|
||||
}
|
||||
return `${name} (${convertChannelTypeToLabel(channelType)})`;
|
||||
};
|
||||
|
||||
export const buildContactableInboxesList = contactInboxes => {
|
||||
if (!contactInboxes) return [];
|
||||
return contactInboxes.map(
|
||||
({ name, id, email, channelType, phoneNumber, ...rest }) => ({
|
||||
id,
|
||||
label: generateLabelForContactableInboxesList({
|
||||
name,
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
}),
|
||||
action: 'inbox',
|
||||
value: id,
|
||||
name,
|
||||
email,
|
||||
phoneNumber,
|
||||
channelType,
|
||||
...rest,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getCapitalizedNameFromEmail = email => {
|
||||
const name = email.match(/^([^@]*)@/)?.[1] || email.split('@')[0];
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
};
|
||||
|
||||
export const processContactableInboxes = inboxes => {
|
||||
return inboxes.map(inbox => ({
|
||||
...inbox.inbox,
|
||||
sourceId: inbox.sourceId,
|
||||
}));
|
||||
};
|
||||
|
||||
export const prepareAttachmentPayload = (
|
||||
attachedFiles,
|
||||
directUploadsEnabled
|
||||
) => {
|
||||
const files = [];
|
||||
attachedFiles.forEach(attachment => {
|
||||
if (directUploadsEnabled) {
|
||||
files.push(attachment.blobSignedId);
|
||||
} else {
|
||||
files.push(attachment.resource.file);
|
||||
}
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
export const prepareNewMessagePayload = ({
|
||||
targetInbox,
|
||||
selectedContact,
|
||||
message,
|
||||
subject,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
currentUser,
|
||||
attachedFiles = [],
|
||||
directUploadsEnabled = false,
|
||||
}) => {
|
||||
const payload = {
|
||||
inboxId: targetInbox.id,
|
||||
sourceId: targetInbox.sourceId,
|
||||
contactId: Number(selectedContact.id),
|
||||
message: { content: message },
|
||||
assigneeId: currentUser.id,
|
||||
};
|
||||
|
||||
if (attachedFiles?.length) {
|
||||
payload.files = prepareAttachmentPayload(
|
||||
attachedFiles,
|
||||
directUploadsEnabled
|
||||
);
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
payload.mailSubject = subject;
|
||||
}
|
||||
|
||||
if (ccEmails) {
|
||||
payload.message.cc_emails = ccEmails;
|
||||
}
|
||||
|
||||
if (bccEmails) {
|
||||
payload.message.bcc_emails = bccEmails;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
export const prepareWhatsAppMessagePayload = ({
|
||||
targetInbox,
|
||||
selectedContact,
|
||||
message,
|
||||
templateParams,
|
||||
currentUser,
|
||||
}) => {
|
||||
return {
|
||||
inboxId: targetInbox.id,
|
||||
sourceId: targetInbox.sourceId,
|
||||
contactId: selectedContact.id,
|
||||
message: { content: message, template_params: templateParams },
|
||||
assigneeId: currentUser.id,
|
||||
};
|
||||
};
|
||||
|
||||
// API Calls
|
||||
export const createNewContact = async email => {
|
||||
const payload = {
|
||||
name: getCapitalizedNameFromEmail(email),
|
||||
email,
|
||||
};
|
||||
|
||||
const {
|
||||
data: {
|
||||
payload: { contact: newContact },
|
||||
},
|
||||
} = await ContactAPI.create(payload);
|
||||
|
||||
return camelcaseKeys(newContact, { deep: true });
|
||||
};
|
||||
|
||||
export const fetchContactableInboxes = async contactId => {
|
||||
const {
|
||||
data: { payload: inboxes = [] },
|
||||
} = await ContactAPI.getContactableInboxes(contactId);
|
||||
|
||||
const convertInboxesToCamelKeys = camelcaseKeys(inboxes, { deep: true });
|
||||
|
||||
return processContactableInboxes(convertInboxesToCamelKeys);
|
||||
};
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import ContactAPI from 'dashboard/api/contacts';
|
||||
import * as helpers from '../composeConversationHelper';
|
||||
|
||||
vi.mock('dashboard/api/contacts');
|
||||
|
||||
describe('composeConversationHelper', () => {
|
||||
describe('convertChannelTypeToLabel', () => {
|
||||
it('converts channel type with namespace to capitalized label', () => {
|
||||
expect(helpers.convertChannelTypeToLabel('Channel::Email')).toBe('Email');
|
||||
expect(helpers.convertChannelTypeToLabel('Channel::Whatsapp')).toBe(
|
||||
'Whatsapp'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns original value if no namespace found', () => {
|
||||
expect(helpers.convertChannelTypeToLabel('email')).toBe('email');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateLabelForContactableInboxesList', () => {
|
||||
const contact = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phoneNumber: '+1234567890',
|
||||
};
|
||||
|
||||
it('generates label for email inbox', () => {
|
||||
expect(
|
||||
helpers.generateLabelForContactableInboxesList({
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.EMAIL,
|
||||
})
|
||||
).toBe('John Doe (john@example.com)');
|
||||
});
|
||||
|
||||
it('generates label for twilio inbox', () => {
|
||||
expect(
|
||||
helpers.generateLabelForContactableInboxesList({
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.TWILIO,
|
||||
})
|
||||
).toBe('John Doe (+1234567890)');
|
||||
});
|
||||
|
||||
it('generates label for whatsapp inbox', () => {
|
||||
expect(
|
||||
helpers.generateLabelForContactableInboxesList({
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.WHATSAPP,
|
||||
})
|
||||
).toBe('John Doe (+1234567890)');
|
||||
});
|
||||
|
||||
it('generates label for other inbox types', () => {
|
||||
expect(
|
||||
helpers.generateLabelForContactableInboxesList({
|
||||
...contact,
|
||||
channelType: 'Channel::Api',
|
||||
})
|
||||
).toBe('John Doe (Api)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildContactableInboxesList', () => {
|
||||
it('returns empty array if no contact inboxes', () => {
|
||||
expect(helpers.buildContactableInboxesList(null)).toEqual([]);
|
||||
expect(helpers.buildContactableInboxesList(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('builds list of contactable inboxes with correct format', () => {
|
||||
const inboxes = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Email Inbox',
|
||||
email: 'support@example.com',
|
||||
channelType: INBOX_TYPES.EMAIL,
|
||||
phoneNumber: null,
|
||||
},
|
||||
];
|
||||
|
||||
const result = helpers.buildContactableInboxesList(inboxes);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 1,
|
||||
label: 'Email Inbox (support@example.com)',
|
||||
action: 'inbox',
|
||||
value: 1,
|
||||
name: 'Email Inbox',
|
||||
email: 'support@example.com',
|
||||
channelType: INBOX_TYPES.EMAIL,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCapitalizedNameFromEmail', () => {
|
||||
it('extracts and capitalizes name from email', () => {
|
||||
expect(helpers.getCapitalizedNameFromEmail('john.doe@example.com')).toBe(
|
||||
'John.doe'
|
||||
);
|
||||
expect(helpers.getCapitalizedNameFromEmail('jane@example.com')).toBe(
|
||||
'Jane'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processContactableInboxes', () => {
|
||||
it('processes inboxes with correct structure', () => {
|
||||
const inboxes = [
|
||||
{
|
||||
inbox: { id: 1, name: 'Inbox 1' },
|
||||
sourceId: 'source1',
|
||||
},
|
||||
];
|
||||
|
||||
const result = helpers.processContactableInboxes(inboxes);
|
||||
expect(result[0]).toEqual({
|
||||
id: 1,
|
||||
name: 'Inbox 1',
|
||||
sourceId: 'source1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareAttachmentPayload', () => {
|
||||
it('prepares direct upload files', () => {
|
||||
const files = [{ blobSignedId: 'signed1' }];
|
||||
expect(helpers.prepareAttachmentPayload(files, true)).toEqual([
|
||||
'signed1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prepares regular files', () => {
|
||||
const files = [{ resource: { file: 'file1' } }];
|
||||
expect(helpers.prepareAttachmentPayload(files, false)).toEqual(['file1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareNewMessagePayload', () => {
|
||||
const baseParams = {
|
||||
targetInbox: { id: 1, sourceId: 'source1' },
|
||||
selectedContact: { id: '2' },
|
||||
message: 'Hello',
|
||||
currentUser: { id: 3 },
|
||||
};
|
||||
|
||||
it('prepares basic message payload', () => {
|
||||
const result = helpers.prepareNewMessagePayload(baseParams);
|
||||
expect(result).toEqual({
|
||||
inboxId: 1,
|
||||
sourceId: 'source1',
|
||||
contactId: 2,
|
||||
message: { content: 'Hello' },
|
||||
assigneeId: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('includes optional fields when provided', () => {
|
||||
const result = helpers.prepareNewMessagePayload({
|
||||
...baseParams,
|
||||
subject: 'Test',
|
||||
ccEmails: 'cc@test.com',
|
||||
bccEmails: 'bcc@test.com',
|
||||
attachedFiles: [{ blobSignedId: 'file1' }],
|
||||
directUploadsEnabled: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
mailSubject: 'Test',
|
||||
message: {
|
||||
content: 'Hello',
|
||||
cc_emails: 'cc@test.com',
|
||||
bcc_emails: 'bcc@test.com',
|
||||
},
|
||||
files: ['file1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareWhatsAppMessagePayload', () => {
|
||||
it('prepares whatsapp message payload', () => {
|
||||
const params = {
|
||||
targetInbox: { id: 1, sourceId: 'source1' },
|
||||
selectedContact: { id: 2 },
|
||||
message: 'Hello',
|
||||
templateParams: { param1: 'value1' },
|
||||
currentUser: { id: 3 },
|
||||
};
|
||||
|
||||
const result = helpers.prepareWhatsAppMessagePayload(params);
|
||||
expect(result).toEqual({
|
||||
inboxId: 1,
|
||||
sourceId: 'source1',
|
||||
contactId: 2,
|
||||
message: {
|
||||
content: 'Hello',
|
||||
template_params: { param1: 'value1' },
|
||||
},
|
||||
assigneeId: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
describe('createNewContact', () => {
|
||||
it('creates new contact with capitalized name', async () => {
|
||||
const mockContact = { id: 1, name: 'John', email: 'john@example.com' };
|
||||
ContactAPI.create.mockResolvedValue({
|
||||
data: { payload: { contact: mockContact } },
|
||||
});
|
||||
|
||||
const result = await helpers.createNewContact('john@example.com');
|
||||
expect(result).toEqual(mockContact);
|
||||
expect(ContactAPI.create).toHaveBeenCalledWith({
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchContactableInboxes', () => {
|
||||
it('fetches and processes contactable inboxes', async () => {
|
||||
const mockInboxes = [
|
||||
{
|
||||
inbox: { id: 1, name: 'Inbox 1' },
|
||||
sourceId: 'source1',
|
||||
},
|
||||
];
|
||||
ContactAPI.getContactableInboxes.mockResolvedValue({
|
||||
data: { payload: mockInboxes },
|
||||
});
|
||||
|
||||
const result = await helpers.fetchContactableInboxes(1);
|
||||
expect(result[0]).toEqual({
|
||||
id: 1,
|
||||
name: 'Inbox 1',
|
||||
sourceId: 'source1',
|
||||
});
|
||||
expect(ContactAPI.getContactableInboxes).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('returns empty array when no inboxes found', async () => {
|
||||
ContactAPI.getContactableInboxes.mockResolvedValue({
|
||||
data: { payload: [] },
|
||||
});
|
||||
|
||||
const result = await helpers.fetchContactableInboxes(1);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user