feat: Add ability to create a new conversation if the previous conversation is resolved (#2512)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Muhsin Keloth 2021-06-30 21:09:44 +05:30 committed by GitHub
parent e6e9916fdb
commit f0f66c7da6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 119 additions and 16 deletions

View file

@ -2,4 +2,5 @@ export const BUS_EVENTS = {
SET_REFERRER_HOST: 'SET_REFERRER_HOST',
SET_TWEET_REPLY: 'SET_TWEET_REPLY',
ATTACHMENT_SIZE_CHECK_ERROR: 'ATTACHMENT_SIZE_CHECK_ERROR',
START_NEW_CONVERSATION: 'START_NEW_CONVERSATION',
};

View file

@ -77,7 +77,7 @@ export default {
this.registerListeners();
this.sendRNWebViewLoadedEvent();
}
this.$store.dispatch('conversationAttributes/get');
this.$store.dispatch('conversationAttributes/getAttributes');
this.setWidgetColor(window.chatwootWebChannel);
this.registerUnreadEvents();
this.registerCampaignEvents();

View file

@ -1,19 +1,33 @@
<template>
<footer class="footer">
<footer v-if="!hideReplyBox" class="footer">
<ChatInputWrap
:on-send-message="handleSendMessage"
:on-send-attachment="handleSendAttachment"
/>
</footer>
<custom-button
v-else
class="font-medium"
block
:bg-color="widgetColor"
:text-color="textColor"
@click="startNewConversation"
>
{{ $t('START_NEW_CONVERSATION') }}
</custom-button>
</template>
<script>
import { mapActions } from 'vuex';
import { mapActions, mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import CustomButton from 'shared/components/Button';
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
export default {
components: {
ChatInputWrap,
CustomButton,
},
props: {
msg: {
@ -21,16 +35,49 @@ export default {
default: '',
},
},
computed: {
...mapGetters({
conversationAttributes: 'conversationAttributes/getConversationParams',
widgetColor: 'appConfig/getWidgetColor',
getConversationSize: 'conversation/getConversationSize',
}),
textColor() {
return getContrastingTextColor(this.widgetColor);
},
hideReplyBox() {
const { csatSurveyEnabled } = window.chatwootWebChannel;
const { status } = this.conversationAttributes;
return csatSurveyEnabled && status === 'resolved';
},
},
methods: {
...mapActions('conversation', ['sendMessage', 'sendAttachment']),
handleSendMessage(content) {
this.sendMessage({
...mapActions('conversation', [
'sendMessage',
'sendAttachment',
'clearConversations',
]),
...mapActions('conversationAttributes', [
'getAttributes',
'clearConversationAttributes',
]),
async handleSendMessage(content) {
const conversationSize = this.getConversationSize;
await this.sendMessage({
content,
});
// Update conversation attributes on new conversation
if (conversationSize === 0) {
this.getAttributes();
}
},
handleSendAttachment(attachment) {
this.sendAttachment({ attachment });
},
startNewConversation() {
this.clearConversations();
this.clearConversationAttributes();
window.bus.$emit(BUS_EVENTS.START_NEW_CONVERSATION);
},
},
};
</script>

View file

@ -18,6 +18,7 @@
"IN_A_DAY": "Typically replies in a day"
},
"START_CONVERSATION": "Start Conversation",
"START_NEW_CONVERSATION": "Start a new conversation",
"UNREAD_VIEW": {
"VIEW_MESSAGES_BUTTON": "See new messages",
"CLOSE_MESSAGES_BUTTON": "Close",

View file

@ -22,10 +22,16 @@ export default {
return window.chatwootWebChannel.preChatFormEnabled;
},
preChatFormOptions() {
let requireEmail = false;
let preChatMessage = '';
const options = window.chatwootWebChannel.preChatFormOptions || {};
if (!this.isOnNewConversation) {
requireEmail = options.require_email;
preChatMessage = options.pre_chat_message;
}
return {
requireEmail: options.require_email,
preChatMessage: options.pre_chat_message,
requireEmail,
preChatMessage,
};
},
},

View file

@ -6,6 +6,7 @@ global.chatwootWebChannel = {
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: ['emoji_picker', 'attachments'],
preChatFormOptions: { require_email: false, pre_chat_message: '' },
};
global.chatwootWidgetDefaults = {
@ -31,6 +32,14 @@ describe('configMixin', () => {
avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: ['emoji_picker', 'attachments'],
preChatFormOptions: {
pre_chat_message: '',
require_email: false,
},
});
expect(wrapper.vm.preChatFormOptions).toEqual({
requireEmail: false,
preChatMessage: '',
});
});
});

View file

@ -11,7 +11,7 @@ import { refreshActionCableConnector } from '../../../helpers/actionCable';
import { createTemporaryMessage, onNewMessageCreated } from './helpers';
export const actions = {
createConversation: async ({ commit }, params) => {
createConversation: async ({ commit, dispatch }, params) => {
commit('setConversationUIFlag', { isCreating: true });
try {
const { data } = await createConversationAPI(params);
@ -22,6 +22,7 @@ export const actions = {
const [message = {}] = messages;
commit('pushMessageToConversation', message);
refreshActionCableConnector(pubsubToken);
dispatch('conversationAttributes/getAttributes', {}, { root: true });
} catch (error) {
console.log(error);
// Ignore error

View file

@ -1,6 +1,7 @@
import {
SET_CONVERSATION_ATTRIBUTES,
UPDATE_CONVERSATION_ATTRIBUTES,
CLEAR_CONVERSATION_ATTRIBUTES,
} from '../types';
import { getConversationAPI } from '../../api/conversation';
@ -14,7 +15,7 @@ export const getters = {
};
export const actions = {
get: async ({ commit }) => {
getAttributes: async ({ commit }) => {
try {
const { data } = await getConversationAPI();
const { contact_last_seen_at: lastSeen } = data;
@ -27,6 +28,9 @@ export const actions = {
update({ commit }, data) {
commit(UPDATE_CONVERSATION_ATTRIBUTES, data);
},
clearConversationAttributes: ({ commit }) => {
commit('CLEAR_CONVERSATION_ATTRIBUTES');
},
};
export const mutations = {
@ -40,6 +44,10 @@ export const mutations = {
$state.status = data.status;
}
},
[CLEAR_CONVERSATION_ATTRIBUTES]($state) {
$state.id = '';
$state.status = '';
},
};
export default {

View file

@ -5,10 +5,10 @@ const commit = jest.fn();
jest.mock('widget/helpers/axios');
describe('#actions', () => {
describe('#update', () => {
describe('#get attributes', () => {
it('sends mutation if api is success', async () => {
API.get.mockResolvedValue({ data: { id: 1, status: 'bot' } });
await actions.get({ commit });
await actions.getAttributes({ commit });
expect(commit.mock.calls).toEqual([
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'bot' }],
['conversation/setMetaUserLastSeenAt', undefined, { root: true }],
@ -16,12 +16,12 @@ describe('#actions', () => {
});
it('doesnot send mutation if api is error', async () => {
API.get.mockRejectedValue({ message: 'Invalid Headers' });
await actions.get({ commit });
await actions.getAttributes({ commit });
expect(commit.mock.calls).toEqual([]);
});
});
describe('#update', () => {
describe('#update attributes', () => {
it('sends correct mutations', () => {
actions.update({ commit }, { id: 1, status: 'bot' });
expect(commit).toBeCalledWith('UPDATE_CONVERSATION_ATTRIBUTES', {
@ -30,4 +30,10 @@ describe('#actions', () => {
});
});
});
describe('#clear attributes', () => {
it('sends correct mutations', () => {
actions.clearConversationAttributes({ commit });
expect(commit).toBeCalledWith('CLEAR_CONVERSATION_ATTRIBUTES');
});
});
});

View file

@ -30,4 +30,15 @@ describe('#mutations', () => {
expect(state).toEqual({ id: 1, status: 'bot' });
});
});
describe('#CLEAR_CONVERSATION_ATTRIBUTES', () => {
it('clear status if it is same conversation', () => {
const state = { id: 1, status: 'open' };
mutations.CLEAR_CONVERSATION_ATTRIBUTES(state, {
id: 1,
status: 'open',
});
expect(state).toEqual({ id: '', status: '' });
});
});
});

View file

@ -1,3 +1,4 @@
export const SET_WIDGET_COLOR = 'SET_WIDGET_COLOR';
export const SET_CONVERSATION_ATTRIBUTES = 'SET_CONVERSATION_ATTRIBUTES';
export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES';
export const CLEAR_CONVERSATION_ATTRIBUTES = 'CLEAR_CONVERSATION_ATTRIBUTES';

View file

@ -113,7 +113,11 @@ export default {
},
},
data() {
return { isOnCollapsedView: false, showAttachmentError: false };
return {
isOnCollapsedView: false,
showAttachmentError: false,
isOnNewConversation: false,
};
},
computed: {
...mapGetters({
@ -130,7 +134,10 @@ export default {
if (this.conversationSize) {
return 'messageView';
}
if (this.preChatFormEnabled && !currentUserEmail) {
if (
this.isOnNewConversation ||
(this.preChatFormEnabled && !currentUserEmail)
) {
return 'preChatFormView';
}
return 'messageView';
@ -163,6 +170,10 @@ export default {
this.showAttachmentError = false;
}, 3000);
});
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
this.isOnCollapsedView = true;
this.isOnNewConversation = true;
});
},
methods: {
startConversation() {

View file

@ -20,6 +20,7 @@
preChatFormEnabled: <%= @web_widget.pre_chat_form_enabled %>,
preChatFormOptions: <%= @web_widget.pre_chat_form_options.to_json.html_safe %>,
workingHoursEnabled: <%= @web_widget.inbox.working_hours_enabled %>,
csatSurveyEnabled: <%= @web_widget.inbox.csat_survey_enabled %>,
workingHours: <%= @web_widget.inbox.working_hours.to_json.html_safe %>,
outOfOfficeMessage: <%= @web_widget.inbox.out_of_office_message.to_json.html_safe %>,
utcOffset: '<%= ActiveSupport::TimeZone[@web_widget.inbox.timezone].formatted_offset %>'