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: { computed: {
buttonClassName() { buttonClassName() {
let className = 'text-white py-3 px-4 rounded shadow-sm'; 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) { if (this.type === 'blue' && !Object.keys(this.buttonStyles).length) {
className = `${className} bg-woot-500 hover:bg-woot-700`; className = `${className} bg-woot-500 hover:bg-woot-700`;
} }

View file

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

View file

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

View file

@ -49,6 +49,7 @@ $color-white: #fff;
$color-body: #3c4858; $color-body: #3c4858;
$color-heading: #1f2d3d; $color-heading: #1f2d3d;
$color-error: #ff382d; $color-error: #ff382d;
$color-success: #44ce4b;
// Color-palettes // 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() { data() {
return { isUploading: false }; return { isUploading: false };
}, },
computed: {
fileUploadSizeLimit() {
return MAXIMUM_FILE_UPLOAD_SIZE;
},
},
methods: { methods: {
getFileType(fileType) { getFileType(fileType) {
return fileType.includes('image') ? 'image' : 'file'; return fileType.includes('image') ? 'image' : 'file';
@ -47,7 +52,11 @@ export default {
thumbUrl, thumbUrl,
}); });
} else { } 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) { } catch (error) {
// Error // Error

View file

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

View file

@ -62,5 +62,10 @@
"TITLE": "Rate your conversation", "TITLE": "Rate your conversation",
"SUBMITTED_TITLE": "Thank you for submitting the rating", "SUBMITTED_TITLE": "Thank you for submitting the rating",
"PLACEHOLDER": "Tell us more..." "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, getUIFlags: $state => $state.uiFlags,
}; };
const actions = { export const actions = {
update: async ({ commit }, { email, messageId, submittedValues }) => { update: async (
{ commit, dispatch },
{ email, messageId, submittedValues }
) => {
commit('toggleUpdateStatus', true); commit('toggleUpdateStatus', true);
try { try {
const { const {
@ -33,6 +36,7 @@ const actions = {
}, },
{ root: true } { root: true }
); );
dispatch('contacts/get', {}, { root: true });
refreshActionCableConnector(pubsubToken); refreshActionCableConnector(pubsubToken);
} catch (error) { } catch (error) {
// Ignore error // Ignore error
@ -41,7 +45,7 @@ const actions = {
}, },
}; };
const mutations = { export const mutations = {
toggleUpdateStatus($state, status) { toggleUpdateStatus($state, status) {
$state.uiFlags.isUpdating = 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> </transition>
</div> </div>
<div v-if="showAttachmentError" class="banner"> <banner />
<span>
{{
$t('FILE_SIZE_LIMIT', {
MAXIMUM_FILE_UPLOAD_SIZE: fileUploadSizeLimit,
})
}}
</span>
</div>
<div class="flex flex-1 overflow-auto"> <div class="flex flex-1 overflow-auto">
<conversation-wrap <conversation-wrap
v-if="currentView === 'messageView'" v-if="currentView === 'messageView'"
@ -85,6 +77,7 @@ import ConversationWrap from 'widget/components/ConversationWrap.vue';
import configMixin from '../mixins/configMixin'; import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability'; import TeamAvailability from 'widget/components/TeamAvailability';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import Banner from 'widget/components/Banner.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages'; import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
@ -100,6 +93,7 @@ export default {
PreChatForm, PreChatForm,
Spinner, Spinner,
TeamAvailability, TeamAvailability,
Banner,
}, },
mixins: [configMixin], mixins: [configMixin],
props: { props: {
@ -115,7 +109,6 @@ export default {
data() { data() {
return { return {
isOnCollapsedView: false, isOnCollapsedView: false,
showAttachmentError: false,
isOnNewConversation: false, isOnNewConversation: false,
}; };
}, },
@ -164,12 +157,6 @@ export default {
}, },
}, },
mounted() { mounted() {
bus.$on(BUS_EVENTS.ATTACHMENT_SIZE_CHECK_ERROR, () => {
this.showAttachmentError = true;
setTimeout(() => {
this.showAttachmentError = false;
}, 3000);
});
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => { bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
this.isOnCollapsedView = true; this.isOnCollapsedView = true;
this.isOnNewConversation = true; this.isOnNewConversation = true;
@ -242,13 +229,5 @@ export default {
.input-wrap { .input-wrap {
padding: 0 $space-normal; 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> </style>