-
+
+
+
+
+ {{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }}
+
+
+ {{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.HELP_TEXT') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
+
+
+
@@ -169,6 +233,11 @@ import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
import { getInboxSource } from 'dashboard/helper/inbox';
import { required, requiredIf } from 'vuelidate/lib/validators';
+import inboxMixin from 'shared/mixins/inboxMixin';
+import FileUpload from 'vue-upload-component';
+import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
+import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
+import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
export default {
components: {
@@ -178,8 +247,10 @@ export default {
CannedResponse,
WhatsappTemplates,
InboxDropdownItem,
+ FileUpload,
+ AttachmentPreview,
},
- mixins: [alertMixin],
+ mixins: [alertMixin, inboxMixin, fileUploadMixin],
props: {
contact: {
type: Object,
@@ -201,6 +272,7 @@ export default {
ccEmails: '',
targetInbox: {},
whatsappTemplateSelected: false,
+ attachedFiles: [],
};
},
validations: {
@@ -219,8 +291,9 @@ export default {
uiFlags: 'contacts/getUIFlags',
conversationsUiFlags: 'contactConversations/getUIFlags',
currentUser: 'getCurrentUser',
+ globalConfig: 'globalConfig/get',
}),
- emailMessagePayload() {
+ newMessagePayload() {
const payload = {
inboxId: this.targetInbox.id,
sourceId: this.targetInbox.sourceId,
@@ -229,6 +302,12 @@ export default {
mailSubject: this.subject,
assigneeId: this.currentUser.id,
};
+
+ if (this.attachedFiles && this.attachedFiles.length) {
+ payload.files = [];
+ this.setAttachmentPayload(payload);
+ }
+
if (this.ccEmails) {
payload.message.cc_emails = this.ccEmails;
}
@@ -284,6 +363,15 @@ export default {
hasWhatsappTemplates() {
return !!this.selectedInbox.inbox?.message_templates;
},
+ hasAttachments() {
+ return this.attachedFiles.length;
+ },
+ inbox() {
+ return this.targetInbox;
+ },
+ allowedFileTypes() {
+ return ALLOWED_FILE_TYPES;
+ },
},
watch: {
message(value) {
@@ -300,6 +388,33 @@ export default {
},
},
methods: {
+ setAttachmentPayload(payload) {
+ this.attachedFiles.forEach(attachment => {
+ if (this.globalConfig.directUploadsEnabled) {
+ payload.files.push(attachment.blobSignedId);
+ } else {
+ payload.files.push(attachment.resource.file);
+ }
+ });
+ },
+ attachFile({ blob, file }) {
+ const reader = new FileReader();
+ reader.readAsDataURL(file.file);
+ reader.onloadend = () => {
+ this.attachedFiles.push({
+ currentChatId: this.contact.id,
+ resource: blob || file,
+ isPrivate: this.isPrivate,
+ thumb: reader.result,
+ blobSignedId: blob ? blob.signed_id : undefined,
+ });
+ };
+ },
+ removeAttachment(itemIndex) {
+ this.attachedFiles = this.attachedFiles.filter(
+ (item, index) => itemIndex !== index
+ );
+ },
onCancel() {
this.$emit('cancel');
},
@@ -320,6 +435,10 @@ export default {
message: { content, template_params: templateParams },
assigneeId: this.currentUser.id,
};
+ if (this.attachedFiles && this.attachedFiles.length) {
+ payload.files = [];
+ this.setAttachmentPayload(payload);
+ }
return payload;
},
onFormSubmit() {
@@ -327,7 +446,7 @@ export default {
if (this.$v.$invalid) {
return;
}
- this.createConversation(this.emailMessagePayload);
+ this.createConversation(this.newMessagePayload);
},
async createConversation(payload) {
try {
@@ -389,6 +508,18 @@ export default {
}
}
+.file-uploads {
+ @apply text-start;
+}
+
+.multiselect-wrap--small.has-multi-select-error {
+ ::v-deep {
+ .multiselect__tags {
+ @apply border-red-500;
+ }
+ }
+}
+
::v-deep {
.mention--box {
@apply left-0 m-auto right-0 top-auto h-fit;
diff --git a/app/javascript/dashboard/store/modules/contactConversations.js b/app/javascript/dashboard/store/modules/contactConversations.js
index a696a3e75..94158735e 100644
--- a/app/javascript/dashboard/store/modules/contactConversations.js
+++ b/app/javascript/dashboard/store/modules/contactConversations.js
@@ -3,6 +3,34 @@ import * as types from '../mutation-types';
import ContactAPI from '../../api/contacts';
import ConversationApi from '../../api/conversations';
+export const createMessagePayload = (payload, message) => {
+ const { content, cc_emails, bcc_emails } = message;
+ payload.append('message[content]', content);
+ if (cc_emails) payload.append('message[cc_emails]', cc_emails);
+ if (bcc_emails) payload.append('message[bcc_emails]', bcc_emails);
+};
+
+export const createConversationPayload = ({ params, contactId, files }) => {
+ const { inboxId, message, sourceId, mailSubject, assigneeId } = params;
+ const payload = new FormData();
+
+ if (message) {
+ createMessagePayload(payload, message);
+ }
+
+ if (files && files.length > 0) {
+ files.forEach(file => payload.append('message[attachments][]', file));
+ }
+
+ payload.append('inbox_id', inboxId);
+ payload.append('contact_id', contactId);
+ payload.append('source_id', sourceId);
+ payload.append('additional_attributes[mail_subject]', mailSubject);
+ payload.append('assignee_id', assigneeId);
+
+ return payload;
+};
+
const state = {
records: {},
uiFlags: {
@@ -24,29 +52,17 @@ export const actions = {
commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, {
isCreating: true,
});
- const {
- inboxId,
- message,
- contactId,
- sourceId,
- mailSubject,
- assigneeId,
- } = params;
+ const { contactId, files } = params;
+
try {
- const { data } = await ConversationApi.create({
- inbox_id: inboxId,
- contact_id: contactId,
- source_id: sourceId,
- additional_attributes: {
- mail_subject: mailSubject,
- },
- message,
- assignee_id: assigneeId,
- });
+ const payload = createConversationPayload({ params, contactId, files });
+
+ const { data } = await ConversationApi.create(payload);
commit(types.default.ADD_CONTACT_CONVERSATION, {
id: contactId,
data,
});
+
return data;
} catch (error) {
throw new Error(error);
diff --git a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js
index 0de32311b..8524f7bce 100644
--- a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js
@@ -1,5 +1,9 @@
import axios from 'axios';
-import { actions } from '../../contactConversations';
+import {
+ actions,
+ createMessagePayload,
+ createConversationPayload,
+} from '../../contactConversations';
import * as types from '../../../mutation-types';
import conversationList from './fixtures';
@@ -49,6 +53,35 @@ describe('#actions', () => {
contactId: 4,
sourceId: 5,
mailSubject: 'Mail Subject',
+ assigneeId: 6,
+ files: [],
+ }
+ );
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isCreating: true }],
+
+ [
+ types.default.ADD_CONTACT_CONVERSATION,
+ { id: 4, data: conversationList[0] },
+ ],
+ [
+ types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG,
+ { isCreating: false },
+ ],
+ ]);
+ });
+ it('sends correct actions with files if API is success', async () => {
+ axios.post.mockResolvedValue({ data: conversationList[0] });
+ await actions.create(
+ { commit },
+ {
+ inboxId: 1,
+ message: { content: 'hi' },
+ contactId: 4,
+ sourceId: 5,
+ assigneeId: 6,
+ mailSubject: 'Mail Subject',
+ files: [new File([], 'file1')],
}
);
expect(commit.mock.calls).toEqual([
@@ -74,6 +107,7 @@ describe('#actions', () => {
inboxId: 1,
message: { content: 'hi' },
contactId: 4,
+ assigneeId: 6,
sourceId: 5,
mailSubject: 'Mail Subject',
}
@@ -87,5 +121,121 @@ describe('#actions', () => {
],
]);
});
+ it('sends correct actions with files if API is error', async () => {
+ axios.post.mockRejectedValue({ message: 'Incorrect header' });
+
+ await expect(
+ actions.create(
+ { commit },
+ {
+ inboxId: 1,
+ message: { content: 'hi' },
+ contactId: 4,
+ assigneeId: 6,
+ sourceId: 5,
+ mailSubject: 'Mail Subject',
+ files: [new File([], 'file1')],
+ }
+ )
+ ).rejects.toThrow(Error);
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isCreating: true }],
+ [
+ types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG,
+ { isCreating: false },
+ ],
+ ]);
+ });
+ });
+});
+
+describe('createMessagePayload', () => {
+ it('creates message payload with cc and bcc emails', () => {
+ const payload = new FormData();
+ const message = {
+ content: 'Test message content',
+ cc_emails: 'cc@example.com',
+ bcc_emails: 'bcc@example.com',
+ };
+
+ createMessagePayload(payload, message);
+
+ expect(payload.get('message[content]')).toBe(message.content);
+ expect(payload.get('message[cc_emails]')).toBe(message.cc_emails);
+ expect(payload.get('message[bcc_emails]')).toBe(message.bcc_emails);
+ });
+
+ it('creates message payload without cc and bcc emails', () => {
+ const payload = new FormData();
+ const message = {
+ content: 'Test message content',
+ };
+
+ createMessagePayload(payload, message);
+
+ expect(payload.get('message[content]')).toBe(message.content);
+ expect(payload.get('message[cc_emails]')).toBeNull();
+ expect(payload.get('message[bcc_emails]')).toBeNull();
+ });
+});
+
+describe('createConversationPayload', () => {
+ it('creates conversation payload with message and attachments', () => {
+ const options = {
+ params: {
+ inboxId: '1',
+ message: {
+ content: 'Test message content',
+ },
+ sourceId: '12',
+ mailSubject: 'Test Subject',
+ assigneeId: '123',
+ },
+ contactId: '23',
+ files: ['file1.pdf', 'file2.jpg'],
+ };
+
+ const payload = createConversationPayload(options);
+
+ expect(payload.get('message[content]')).toBe(
+ options.params.message.content
+ );
+ expect(payload.get('inbox_id')).toBe(options.params.inboxId);
+ expect(payload.get('contact_id')).toBe(options.contactId);
+ expect(payload.get('source_id')).toBe(options.params.sourceId);
+ expect(payload.get('additional_attributes[mail_subject]')).toBe(
+ options.params.mailSubject
+ );
+ expect(payload.get('assignee_id')).toBe(options.params.assigneeId);
+ expect(payload.getAll('message[attachments][]')).toEqual(options.files);
+ });
+
+ it('creates conversation payload with message and without attachments', () => {
+ const options = {
+ params: {
+ inboxId: '1',
+ message: {
+ content: 'Test message content',
+ },
+ sourceId: '12',
+ mailSubject: 'Test Subject',
+ assigneeId: '123',
+ },
+ contactId: '23',
+ };
+
+ const payload = createConversationPayload(options);
+
+ expect(payload.get('message[content]')).toBe(
+ options.params.message.content
+ );
+ expect(payload.get('inbox_id')).toBe(options.params.inboxId);
+ expect(payload.get('contact_id')).toBe(options.contactId);
+ expect(payload.get('source_id')).toBe(options.params.sourceId);
+ expect(payload.get('additional_attributes[mail_subject]')).toBe(
+ options.params.mailSubject
+ );
+ expect(payload.get('assignee_id')).toBe(options.params.assigneeId);
+ expect(payload.getAll('message[attachments][]')).toEqual([]);
});
});
diff --git a/app/javascript/shared/constants/busEvents.js b/app/javascript/shared/constants/busEvents.js
index 6c9a21072..6edf96fcd 100644
--- a/app/javascript/shared/constants/busEvents.js
+++ b/app/javascript/shared/constants/busEvents.js
@@ -10,4 +10,5 @@ export const BUS_EVENTS = {
ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL',
WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT',
SHOW_TOAST: 'newToastMessage',
+ NEW_CONVERSATION_MODAL: 'newConversationModal',
};