Feature: Customise widget for bot conversations (#834)
* Feature: Customise widget for bot conversations
This commit is contained in:
@@ -60,6 +60,8 @@ export default {
|
||||
this.$store.dispatch('contacts/update', message);
|
||||
}
|
||||
});
|
||||
|
||||
this.$store.dispatch('conversationAttributes/get');
|
||||
},
|
||||
methods: {
|
||||
...mapActions('appConfig', ['setWidgetColor']),
|
||||
|
||||
@@ -13,12 +13,16 @@ const sendAttachmentAPI = async attachment => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const getConversationAPI = async ({ before }) => {
|
||||
const getMessagesAPI = async ({ before }) => {
|
||||
const urlData = endPoints.getConversation({ before });
|
||||
const result = await API.get(urlData.url, { params: urlData.params });
|
||||
return result;
|
||||
};
|
||||
|
||||
const getConversationAPI = async () => {
|
||||
return API.get(`/api/v1/widget/conversations${window.location.search}`);
|
||||
};
|
||||
|
||||
const toggleTyping = async ({ typingStatus }) => {
|
||||
return API.post(
|
||||
`/api/v1/widget/conversations/toggle_typing${window.location.search}`,
|
||||
@@ -26,4 +30,10 @@ const toggleTyping = async ({ typingStatus }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { sendMessageAPI, getConversationAPI, sendAttachmentAPI, toggleTyping };
|
||||
export {
|
||||
sendMessageAPI,
|
||||
getConversationAPI,
|
||||
getMessagesAPI,
|
||||
sendAttachmentAPI,
|
||||
toggleTyping,
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ import ImageBubble from 'widget/components/ImageBubble';
|
||||
import FileBubble from 'widget/components/FileBubble';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
|
||||
export default {
|
||||
name: 'AgentMessage',
|
||||
@@ -63,7 +64,7 @@ export default {
|
||||
UserMessage,
|
||||
FileBubble,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
mixins: [timeMixin, configMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
@@ -112,11 +113,17 @@ export default {
|
||||
avatarUrl() {
|
||||
// eslint-disable-next-line
|
||||
const BotImage = require('dashboard/assets/images/chatwoot_bot.png');
|
||||
const displayImage = this.useInboxAvatarForBot
|
||||
? this.inboxAvatarUrl
|
||||
: BotImage;
|
||||
|
||||
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
|
||||
return BotImage;
|
||||
return displayImage;
|
||||
}
|
||||
|
||||
return this.message.sender ? this.message.sender.avatar_url : BotImage;
|
||||
return this.message.sender
|
||||
? this.message.sender.avatar_url
|
||||
: displayImage;
|
||||
},
|
||||
hasRecordedResponse() {
|
||||
return (
|
||||
|
||||
@@ -8,9 +8,15 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
'message.updated': this.onMessageUpdated,
|
||||
'conversation.typing_on': this.onTypingOn,
|
||||
'conversation.typing_off': this.onTypingOff,
|
||||
'conversation.resolved': this.onStatusChange,
|
||||
'conversation.opened': this.onStatusChange,
|
||||
};
|
||||
}
|
||||
|
||||
onStatusChange = data => {
|
||||
this.app.$store.dispatch('conversationAttributes/update', data);
|
||||
};
|
||||
|
||||
onMessageCreated = data => {
|
||||
this.app.$store.dispatch('conversation/addMessage', data);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
computed: {
|
||||
hideInputForBotConversations() {
|
||||
return window.chatwootWebChannel.hideInputForBotConversations;
|
||||
},
|
||||
useInboxAvatarForBot() {
|
||||
return window.chatwootWidgetDefaults.useInboxAvatarForBot;
|
||||
},
|
||||
hasAConnectedAgentBot() {
|
||||
return !!window.chatwootWebChannel.hasAConnectedAgentBot;
|
||||
},
|
||||
inboxAvatarUrl() {
|
||||
return window.chatwootWebChannel.avatarUrl;
|
||||
},
|
||||
channelConfig() {
|
||||
return window.chatwootWebChannel;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import configMixin from '../configMixin';
|
||||
import Vue from 'vue';
|
||||
|
||||
global.chatwootWebChannel = {
|
||||
hideInputForBotConversations: true,
|
||||
avatarUrl: 'https://test.url',
|
||||
hasAConnectedAgentBot: 'AgentBot',
|
||||
};
|
||||
|
||||
global.chatwootWidgetDefaults = {
|
||||
useInboxAvatarForBot: true,
|
||||
};
|
||||
|
||||
describe('configMixin', () => {
|
||||
test('returns config', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [configMixin],
|
||||
};
|
||||
const Constructor = Vue.extend(Component);
|
||||
const vm = new Constructor().$mount();
|
||||
const wrapper = createWrapper(vm);
|
||||
expect(wrapper.vm.hideInputForBotConversations).toBe(true);
|
||||
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
|
||||
expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
|
||||
expect(wrapper.vm.inboxAvatarUrl).toBe('https://test.url');
|
||||
expect(wrapper.vm.channelConfig).toEqual({
|
||||
hideInputForBotConversations: true,
|
||||
avatarUrl: 'https://test.url',
|
||||
hasAConnectedAgentBot: 'AgentBot',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import agent from 'widget/store/modules/agent';
|
||||
import appConfig from 'widget/store/modules/appConfig';
|
||||
import contacts from 'widget/store/modules/contacts';
|
||||
import conversation from 'widget/store/modules/conversation';
|
||||
import conversationAttributes from 'widget/store/modules/conversationAttributes';
|
||||
import conversationLabels from 'widget/store/modules/conversationLabels';
|
||||
import events from 'widget/store/modules/events';
|
||||
import message from 'widget/store/modules/message';
|
||||
@@ -16,6 +17,7 @@ export default new Vuex.Store({
|
||||
appConfig,
|
||||
contacts,
|
||||
conversation,
|
||||
conversationAttributes,
|
||||
conversationLabels,
|
||||
events,
|
||||
message,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
sendMessageAPI,
|
||||
getConversationAPI,
|
||||
getMessagesAPI,
|
||||
sendAttachmentAPI,
|
||||
toggleTyping,
|
||||
} from 'widget/api/conversation';
|
||||
@@ -116,7 +116,7 @@ export const actions = {
|
||||
fetchOldConversations: async ({ commit }, { before } = {}) => {
|
||||
try {
|
||||
commit('setConversationListLoading', true);
|
||||
const { data } = await getConversationAPI({ before });
|
||||
const { data } = await getMessagesAPI({ before });
|
||||
commit('setMessagesInConversation', data);
|
||||
commit('setConversationListLoading', false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
SET_CONVERSATION_ATTRIBUTES,
|
||||
UPDATE_CONVERSATION_ATTRIBUTES,
|
||||
} from '../types';
|
||||
import { getConversationAPI } from '../../api/conversation';
|
||||
|
||||
const state = {
|
||||
id: '',
|
||||
status: '',
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getConversationParams: $state => $state,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
get: async ({ commit }) => {
|
||||
try {
|
||||
const { data } = await getConversationAPI();
|
||||
commit(SET_CONVERSATION_ATTRIBUTES, data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
update({ commit }, data) {
|
||||
commit(UPDATE_CONVERSATION_ATTRIBUTES, data);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[SET_CONVERSATION_ATTRIBUTES]($state, data) {
|
||||
$state.id = data.id;
|
||||
$state.status = data.status;
|
||||
},
|
||||
[UPDATE_CONVERSATION_ATTRIBUTES]($state, data) {
|
||||
if (data.id === $state.id) {
|
||||
$state.id = data.id;
|
||||
$state.status = data.status;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { actions } from '../../conversationAttributes';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
const commit = jest.fn();
|
||||
jest.mock('widget/helpers/axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#update', () => {
|
||||
it('sends mutation if api is success', async () => {
|
||||
API.get.mockResolvedValue({ data: { id: 1, status: 'bot' } });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'bot' }],
|
||||
]);
|
||||
});
|
||||
it('doesnot send mutation if api is error', async () => {
|
||||
API.get.mockRejectedValue({ message: 'Invalid Headers' });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct mutations', () => {
|
||||
actions.update({ commit }, { id: 1, status: 'bot' });
|
||||
expect(commit).toBeCalledWith('UPDATE_CONVERSATION_ATTRIBUTES', {
|
||||
id: 1,
|
||||
status: 'bot',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getters } from '../../conversationAttributes';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getConversationParams', () => {
|
||||
const state = {
|
||||
id: 1,
|
||||
status: 'bot',
|
||||
};
|
||||
expect(getters.getConversationParams(state)).toEqual({
|
||||
id: 1,
|
||||
status: 'bot',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { mutations } from '../../conversationAttributes';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CONVERSATION_ATTRIBUTES', () => {
|
||||
it('set status of the conversation', () => {
|
||||
const state = { id: '', status: '' };
|
||||
mutations.SET_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: 1, status: 'open' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION_ATTRIBUTES', () => {
|
||||
it('update status if it is same conversation', () => {
|
||||
const state = { id: 1, status: 'bot' };
|
||||
mutations.UPDATE_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: 1, status: 'open' });
|
||||
});
|
||||
it('doesnot update status if it is not the same conversation', () => {
|
||||
const state = { id: 1, status: 'bot' };
|
||||
mutations.UPDATE_CONVERSATION_ATTRIBUTES(state, {
|
||||
id: 2,
|
||||
status: 'open',
|
||||
});
|
||||
expect(state).toEqual({ id: 1, status: 'bot' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +1,3 @@
|
||||
export const SET_WIDGET_COLOR = 'SET_WIDGET_COLOR';
|
||||
export const SET_CONVERSATION_ATTRIBUTES = 'SET_CONVERSATION_ATTRIBUTES';
|
||||
export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="home">
|
||||
<div class="header-wrap">
|
||||
<ChatHeaderExpanded
|
||||
v-if="isHeaderExpanded"
|
||||
v-if="isHeaderExpanded && !hideWelcomeHeader"
|
||||
:intro-heading="introHeading"
|
||||
:intro-body="introBody"
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
@@ -16,7 +16,7 @@
|
||||
<AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" />
|
||||
<ConversationWrap :grouped-messages="groupedMessages" />
|
||||
<div class="footer-wrap">
|
||||
<div class="input-wrap">
|
||||
<div v-if="showInputTextArea" class="input-wrap">
|
||||
<ChatFooter />
|
||||
</div>
|
||||
<branding></branding>
|
||||
@@ -33,6 +33,7 @@ import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
|
||||
import ChatHeader from 'widget/components/ChatHeader.vue';
|
||||
import ConversationWrap from 'widget/components/ConversationWrap.vue';
|
||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
@@ -44,30 +45,41 @@ export default {
|
||||
Branding,
|
||||
AvailableAgents,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
groupedMessages: 'conversation/getGroupedConversation',
|
||||
conversationSize: 'conversation/getConversationSize',
|
||||
availableAgents: 'agent/availableAgents',
|
||||
hasFetched: 'agent/uiFlags/hasFetched',
|
||||
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||
}),
|
||||
isOpen() {
|
||||
return this.conversationAttributes.status === 'open';
|
||||
},
|
||||
showInputTextArea() {
|
||||
if (this.hideInputForBotConversations) {
|
||||
if (this.isOpen) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isHeaderExpanded() {
|
||||
return this.conversationSize === 0;
|
||||
},
|
||||
channelConfig() {
|
||||
return window.chatwootWebChannel;
|
||||
},
|
||||
showAvailableAgents() {
|
||||
return this.availableAgents.length > 0 && this.conversationSize < 1;
|
||||
},
|
||||
introHeading() {
|
||||
return this.channelConfig.welcomeTitle || 'Hi there ! 🙌🏼';
|
||||
return this.channelConfig.welcomeTitle;
|
||||
},
|
||||
introBody() {
|
||||
return (
|
||||
this.channelConfig.welcomeTagline ||
|
||||
'We make it simple to connect with us. Ask us anything, or share your feedback.'
|
||||
);
|
||||
return this.channelConfig.welcomeTagline;
|
||||
},
|
||||
hideWelcomeHeader() {
|
||||
return !(this.introHeading || this.introBody);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user