feat: Conversation transcript in widget (#2549)
This commit is contained in:
parent
fc4ef1595b
commit
15085cfb98
13 changed files with 200 additions and 46 deletions
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@ $color-white: #fff;
|
|||
$color-body: #3c4858;
|
||||
$color-heading: #1f2d3d;
|
||||
$color-error: #ff382d;
|
||||
$color-success: #44ce4b;
|
||||
|
||||
// Color-palettes
|
||||
|
||||
|
|
48
app/javascript/widget/components/Banner.vue
Normal file
48
app/javascript/widget/components/Banner.vue
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { getters } from '../../message';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isUpdating: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue