diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 6429c53d2..5dd0bcc57 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -14,6 +14,10 @@ class ContactAPI extends ApiClient { return axios.get(`${this.url}/${contactId}/conversations`); } + getContactableInboxes(contactId) { + return axios.get(`${this.url}/${contactId}/contactable_inboxes`); + } + search(search = '', page = 1) { return axios.get(`${this.url}/search?q=${search}&page=${page}`); } diff --git a/app/javascript/dashboard/components/ui/WootButton.vue b/app/javascript/dashboard/components/ui/WootButton.vue index 6820f1bc9..4df156f3a 100644 --- a/app/javascript/dashboard/components/ui/WootButton.vue +++ b/app/javascript/dashboard/components/ui/WootButton.vue @@ -13,7 +13,7 @@ > - + @@ -120,7 +152,7 @@ export default { @import '~dashboard/assets/scss/mixins'; .contact--profile { align-items: flex-start; - padding: var(--space-normal) var(--space-normal) var(--space-large); + padding: var(--space-normal) var(--space-normal); .user-thumbnail-box { margin-right: $space-normal; @@ -164,8 +196,21 @@ export default { font-size: $font-weight-normal; } } +.contact-actions { + margin: var(--space-small) 0; +} +.button.edit-contact { + margin-left: var(--space-two); + padding-left: var(--space-micro); +} -.edit-contact { - margin-left: var(--space-slab); +.button.new-message { + margin-right: var(--space-small); +} + +.contact-actions { + display: flex; + align-items: center; + width: 100%; } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue new file mode 100644 index 000000000..75e333a73 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -0,0 +1,210 @@ + + + + + {{ $t('NEW_CONVERSATION.NO_INBOX') }} + + + + + + + {{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }} + + + {{ contactableInbox.inbox.name }} + + + + {{ $t('NEW_CONVERSATION.FORM.INBOX.ERROR') }} + + + + + + {{ $t('NEW_CONVERSATION.FORM.TO.LABEL') }} + + + + {{ contact.name }} + + + + + + + + + {{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }} + + + {{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }} + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue new file mode 100644 index 000000000..02e9c4f58 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/NewConversation.vue @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/app/javascript/dashboard/store/modules/contactConversations.js b/app/javascript/dashboard/store/modules/contactConversations.js index 7ac37c29d..d483637fc 100644 --- a/app/javascript/dashboard/store/modules/contactConversations.js +++ b/app/javascript/dashboard/store/modules/contactConversations.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import * as types from '../mutation-types'; import ContactAPI from '../../api/contacts'; +import ConversationApi from '../../api/conversations'; const state = { records: {}, @@ -19,6 +20,30 @@ export const getters = { }; export const actions = { + create: async ({ commit }, params) => { + commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { + isCreating: true, + }); + const { inboxId, message, contactId, sourceId } = params; + try { + const { data } = await ConversationApi.create({ + inbox_id: inboxId, + contact_id: contactId, + source_id: sourceId, + message, + }); + commit(types.default.ADD_CONTACT_CONVERSATION, { + id: contactId, + data, + }); + } catch (error) { + throw new Error(error); + } finally { + commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { + isCreating: false, + }); + } + }, get: async ({ commit }, contactId) => { commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isFetching: true, @@ -53,6 +78,10 @@ export const mutations = { [types.default.SET_CONTACT_CONVERSATIONS]: ($state, { id, data }) => { Vue.set($state.records, id, data); }, + [types.default.ADD_CONTACT_CONVERSATION]: ($state, { id, data }) => { + const conversations = $state.records[id] || []; + Vue.set($state.records, id, [...conversations, data]); + }, }; export default { diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js index 039b69d2c..075c82918 100644 --- a/app/javascript/dashboard/store/modules/contacts/actions.js +++ b/app/javascript/dashboard/store/modules/contacts/actions.js @@ -83,6 +83,26 @@ export const actions = { } }, + fetchContactableInbox: async ({ commit }, id) => { + commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true }); + try { + const response = await ContactAPI.getContactableInboxes(id); + const contact = { + id, + contactableInboxes: response.data.payload, + }; + commit(types.SET_CONTACT_ITEM, contact); + } catch (error) { + if (error.response?.data?.message) { + throw new ExceptionWithMessage(error.response.data.message); + } else { + throw new Error(error); + } + } finally { + commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: false }); + } + }, + updatePresence: ({ commit }, data) => { commit(types.UPDATE_CONTACTS_PRESENCE, data); }, diff --git a/app/javascript/dashboard/store/modules/contacts/index.js b/app/javascript/dashboard/store/modules/contacts/index.js index a8bab100a..d0702fb1f 100644 --- a/app/javascript/dashboard/store/modules/contacts/index.js +++ b/app/javascript/dashboard/store/modules/contacts/index.js @@ -11,6 +11,7 @@ const state = { uiFlags: { isFetching: false, isFetchingItem: false, + isFetchingInboxes: false, isUpdating: false, }, }; diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 713ef60b5..753622ccd 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -1,5 +1,6 @@ import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; import * as types from '../mutation-types'; +import { INBOX_TYPES } from 'shared/mixins/inboxMixin'; import InboxesAPI from '../../api/inboxes'; import WebChannel from '../../api/channel/webChannel'; import FBChannel from '../../api/channel/fbChannel'; @@ -41,6 +42,20 @@ export const getters = { getInboxes($state) { return $state.records; }, + getNewConversationInboxes($state) { + return $state.records.filter(inbox => { + const { + channel_type: channelType, + phone_number: phoneNumber = '', + } = inbox; + + const isEmailChannel = channelType === INBOX_TYPES.EMAIL; + const isSmsChannel = + channelType === INBOX_TYPES.TWILIO && + phoneNumber.startsWith('whatsapp'); + return isEmailChannel || isSmsChannel; + }); + }, getInbox: $state => inboxId => { const [inbox] = $state.records.filter( record => record.id === Number(inboxId) 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 77565b5a4..1daaba977 100644 --- a/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contactConversations/actions.spec.js @@ -38,4 +38,43 @@ describe('#actions', () => { ]); }); }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: conversationList[0] }); + await actions.create( + { commit }, + { inboxId: 1, message: { content: 'hi' }, contactId: 4, sourceId: 5 } + ); + 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 if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + + await expect( + actions.create( + { commit }, + { inboxId: 1, message: { content: 'hi' }, contactId: 4, sourceId: 5 } + ) + ).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 }, + ], + ]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/contactConversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/contactConversations/mutations.spec.js index f4f196186..38aef51d7 100644 --- a/app/javascript/dashboard/store/modules/specs/contactConversations/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contactConversations/mutations.spec.js @@ -26,4 +26,17 @@ describe('#mutations', () => { }); }); }); + + describe('#ADD_CONTACT_CONVERSATION', () => { + it('Adds new contact conversation to records', () => { + const state = { records: {} }; + mutations[types.default.ADD_CONTACT_CONVERSATION](state, { + id: 1, + data: { id: 1, contact_id: 1, message: 'hello' }, + }); + expect(state.records).toEqual({ + 1: [{ id: 1, contact_id: 1, message: 'hello' }], + }); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 355058cba..3fc2bb884 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -114,6 +114,7 @@ export default { // Contact Conversation SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG', SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', + ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION', // Conversation Label SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG', diff --git a/app/services/contacts/contactable_inboxes_service.rb b/app/services/contacts/contactable_inboxes_service.rb index fccd88f8d..b40e6cee7 100644 --- a/app/services/contacts/contactable_inboxes_service.rb +++ b/app/services/contacts/contactable_inboxes_service.rb @@ -40,7 +40,7 @@ class Contacts::ContactableInboxesService end def twilio_contactable_inbox(inbox) - return unless @contact.phone_number + return if @contact.phone_number.blank? case inbox.channel.medium when 'sms' diff --git a/app/views/api/v1/models/_contact_inbox.json.jbuilder b/app/views/api/v1/models/_contact_inbox.json.jbuilder index c4b34f15a..b0214d298 100644 --- a/app/views/api/v1/models/_contact_inbox.json.jbuilder +++ b/app/views/api/v1/models/_contact_inbox.json.jbuilder @@ -1,2 +1,4 @@ json.source_id resource.source_id -json.inbox resource.inbox +json.inbox do + json.partial! 'api/v1/models/inbox.json.jbuilder', resource: resource.inbox +end
+ {{ $t('NEW_CONVERSATION.NO_INBOX') }} +