diff --git a/app/javascript/dashboard/components-next/Editor/Editor.vue b/app/javascript/dashboard/components-next/Editor/Editor.vue index 717658d09..9fbe1bc68 100644 --- a/app/javascript/dashboard/components-next/Editor/Editor.vue +++ b/app/javascript/dashboard/components-next/Editor/Editor.vue @@ -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; } } } diff --git a/app/javascript/dashboard/components-next/NewConversation/ComposeNewConversation.vue b/app/javascript/dashboard/components-next/NewConversation/ComposeNewConversation.vue index 5ca07f3f6..f051efe98 100644 --- a/app/javascript/dashboard/components-next/NewConversation/ComposeNewConversation.vue +++ b/app/javascript/dashboard/components-next/NewConversation/ComposeNewConversation.vue @@ -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" /> {}, + 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')" /> diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ContactSelector.vue b/app/javascript/dashboard/components-next/NewConversation/components/ContactSelector.vue index 18c6292f9..7d8300fe2 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ContactSelector.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ContactSelector.vue @@ -1,5 +1,6 @@