feat: Conversation transcript in widget (#2549)

This commit is contained in:
Muhsin Keloth 2021-07-13 11:31:21 +05:30 committed by GitHub
parent fc4ef1595b
commit 15085cfb98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 200 additions and 46 deletions

View file

@ -26,6 +26,10 @@ export default {
computed: {
buttonClassName() {
let className = 'text-white py-3 px-4 rounded shadow-sm';
if (this.type === 'clear') {
className = 'flex mx-auto mt-4 text-xs w-auto text-black-600';
}
if (this.type === 'blue' && !Object.keys(this.buttonStyles).length) {
className = `${className} bg-woot-500 hover:bg-woot-700`;
}

View file

@ -1,6 +1,6 @@
export const BUS_EVENTS = {
SET_REFERRER_HOST: 'SET_REFERRER_HOST',
SET_TWEET_REPLY: 'SET_TWEET_REPLY',
ATTACHMENT_SIZE_CHECK_ERROR: 'ATTACHMENT_SIZE_CHECK_ERROR',
SHOW_ALERT: 'SHOW_ALERT',
START_NEW_CONVERSATION: 'START_NEW_CONVERSATION',
};

View file

@ -42,6 +42,12 @@ const setUserLastSeenAt = async ({ lastSeen }) => {
{ contact_last_seen_at: lastSeen }
);
};
const sendEmailTranscript = async ({ email }) => {
return API.post(
`/api/v1/widget/conversations/transcript${window.location.search}`,
{ email }
);
};
export {
createConversationAPI,
@ -51,4 +57,5 @@ export {
sendAttachmentAPI,
toggleTyping,
setUserLastSeenAt,
sendEmailTranscript,
};

View file

@ -49,6 +49,7 @@ $color-white: #fff;
$color-body: #3c4858;
$color-heading: #1f2d3d;
$color-error: #ff382d;
$color-success: #44ce4b;
// Color-palettes

View file

@ -0,0 +1,48 @@
<template>
<div v-if="showBannerMessage" :class="`banner ${bannerType}`">
<span>
{{ bannerMessage }}
</span>
</div>
</template>
<script>
import { BUS_EVENTS } from 'shared/constants/busEvents';
export default {
data() {
return {
showBannerMessage: false,
bannerMessage: '',
bannerType: 'error',
};
},
mounted() {
bus.$on(BUS_EVENTS.SHOW_ALERT, ({ message, type = 'error' }) => {
this.bannerMessage = message;
this.bannerType = type;
this.showBannerMessage = true;
setTimeout(() => {
this.showBannerMessage = false;
}, 3000);
});
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
.banner {
color: $color-white;
font-size: $font-size-default;
font-weight: $font-weight-bold;
padding: $space-slab;
text-align: center;
&.success {
background: $color-success;
}
&.error {
background: $color-error;
}
}
</style>

View file

@ -29,6 +29,11 @@ export default {
data() {
return { isUploading: false };
},
computed: {
fileUploadSizeLimit() {
return MAXIMUM_FILE_UPLOAD_SIZE;
},
},
methods: {
getFileType(fileType) {
return fileType.includes('image') ? 'image' : 'file';
@ -47,7 +52,11 @@ export default {
thumbUrl,
});
} else {
window.bus.$emit(BUS_EVENTS.ATTACHMENT_SIZE_CHECK_ERROR);
window.bus.$emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('FILE_SIZE_LIMIT', {
MAXIMUM_FILE_UPLOAD_SIZE: this.fileUploadSizeLimit,
}),
});
}
} catch (error) {
// Error

View file

@ -1,20 +1,31 @@
<template>
<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>
<div>
<footer v-if="!hideReplyBox" class="footer">
<ChatInputWrap
:on-send-message="handleSendMessage"
:on-send-attachment="handleSendAttachment"
/>
</footer>
<div v-else>
<custom-button
class="font-medium"
block
:bg-color="widgetColor"
:text-color="textColor"
@click="startNewConversation"
>
{{ $t('START_NEW_CONVERSATION') }}
</custom-button>
<custom-button
v-if="showEmailTranscriptButton"
type="clear"
class="font-normal"
@click="sendTranscript"
>
{{ $t('EMAIL_TRANSCRIPT.BUTTON_TEXT') }}
</custom-button>
</div>
</div>
</template>
<script>
@ -23,6 +34,7 @@ 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';
import { sendEmailTranscript } from 'widget/api/conversation';
export default {
components: {
@ -40,6 +52,7 @@ export default {
conversationAttributes: 'conversationAttributes/getConversationParams',
widgetColor: 'appConfig/getWidgetColor',
getConversationSize: 'conversation/getConversationSize',
currentUser: 'contacts/getCurrentUser',
}),
textColor() {
return getContrastingTextColor(this.widgetColor);
@ -49,6 +62,9 @@ export default {
const { status } = this.conversationAttributes;
return csatSurveyEnabled && status === 'resolved';
},
showEmailTranscriptButton() {
return this.currentUser && this.currentUser.email;
},
},
methods: {
...mapActions('conversation', [
@ -78,6 +94,24 @@ export default {
this.clearConversationAttributes();
window.bus.$emit(BUS_EVENTS.START_NEW_CONVERSATION);
},
async sendTranscript() {
const { email } = this.currentUser;
if (email) {
try {
await sendEmailTranscript({
email,
});
window.bus.$emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'),
type: 'success',
});
} catch (error) {
window.bus.$emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'),
});
}
}
},
},
};
</script>

View file

@ -62,5 +62,10 @@
"TITLE": "Rate your conversation",
"SUBMITTED_TITLE": "Thank you for submitting the rating",
"PLACEHOLDER": "Tell us more..."
},
"EMAIL_TRANSCRIPT": {
"BUTTON_TEXT": "Request a conversation transcript",
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
"SEND_EMAIL_ERROR": "There was an error, please try again"
}
}

View file

@ -7,12 +7,15 @@ const state = {
},
};
const getters = {
export const getters = {
getUIFlags: $state => $state.uiFlags,
};
const actions = {
update: async ({ commit }, { email, messageId, submittedValues }) => {
export const actions = {
update: async (
{ commit, dispatch },
{ email, messageId, submittedValues }
) => {
commit('toggleUpdateStatus', true);
try {
const {
@ -33,6 +36,7 @@ const actions = {
},
{ root: true }
);
dispatch('contacts/get', {}, { root: true });
refreshActionCableConnector(pubsubToken);
} catch (error) {
// Ignore error
@ -41,7 +45,7 @@ const actions = {
},
};
const mutations = {
export const mutations = {
toggleUpdateStatus($state, status) {
$state.uiFlags.isUpdating = status;
},

View file

@ -0,0 +1,38 @@
import { API } from 'widget/helpers/axios';
import { actions } from '../../message';
const commit = jest.fn();
jest.mock('widget/helpers/axios');
describe('#actions', () => {
describe('#update', () => {
it('sends correct actions', async () => {
const user = {
email: 'john@acme.inc',
messageId: 10,
submittedValues: {
email: 'john@acme.inc',
},
};
API.patch.mockResolvedValue({
data: { contact: { pubsub_token: '8npuMUfDgizrwVoqcK1t7FMY' } },
});
await actions.update({ commit }, user);
expect(commit.mock.calls).toEqual([
['toggleUpdateStatus', true],
[
'conversation/updateMessage',
{
id: 10,
content_attributes: {
submitted_email: 'john@acme.inc',
submitted_values: null,
},
},
{ root: true },
],
['toggleUpdateStatus', false],
]);
});
});
});

View file

@ -0,0 +1,14 @@
import { getters } from '../../message';
describe('#getters', () => {
it('getUIFlags', () => {
const state = {
uiFlags: {
isUpdating: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
isUpdating: false,
});
});
});

View file

@ -0,0 +1,11 @@
import { mutations } from '../../message';
describe('#mutations', () => {
describe('#toggleUpdateStatus', () => {
it('set update flags', () => {
const state = { uiFlags: { status: '' } };
mutations.toggleUpdateStatus(state, 'sent');
expect(state.uiFlags.isUpdating).toEqual('sent');
});
});
});

View file

@ -34,15 +34,7 @@
/>
</transition>
</div>
<div v-if="showAttachmentError" class="banner">
<span>
{{
$t('FILE_SIZE_LIMIT', {
MAXIMUM_FILE_UPLOAD_SIZE: fileUploadSizeLimit,
})
}}
</span>
</div>
<banner />
<div class="flex flex-1 overflow-auto">
<conversation-wrap
v-if="currentView === 'messageView'"
@ -85,6 +77,7 @@ import ConversationWrap from 'widget/components/ConversationWrap.vue';
import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability';
import Spinner from 'shared/components/Spinner.vue';
import Banner from 'widget/components/Banner.vue';
import { mapGetters } from 'vuex';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@ -100,6 +93,7 @@ export default {
PreChatForm,
Spinner,
TeamAvailability,
Banner,
},
mixins: [configMixin],
props: {
@ -115,7 +109,6 @@ export default {
data() {
return {
isOnCollapsedView: false,
showAttachmentError: false,
isOnNewConversation: false,
};
},
@ -164,12 +157,6 @@ export default {
},
},
mounted() {
bus.$on(BUS_EVENTS.ATTACHMENT_SIZE_CHECK_ERROR, () => {
this.showAttachmentError = true;
setTimeout(() => {
this.showAttachmentError = false;
}, 3000);
});
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
this.isOnCollapsedView = true;
this.isOnNewConversation = true;
@ -242,13 +229,5 @@ export default {
.input-wrap {
padding: 0 $space-normal;
}
.banner {
background: $color-error;
color: $color-white;
font-size: $font-size-default;
font-weight: $font-weight-bold;
padding: $space-slab;
text-align: center;
}
}
</style>