Feature: Rich Message Types (#610)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com> Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
This commit is contained in:
@@ -9,7 +9,7 @@ class MessageApi extends ApiClient {
|
||||
|
||||
create({ conversationId, message, private: isPrivate }) {
|
||||
return axios.post(`${this.url}/${conversationId}/messages`, {
|
||||
message,
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ export default {
|
||||
DOWNLOAD: 'Herunterladen',
|
||||
UPLOADING: 'Hochladen...',
|
||||
},
|
||||
|
||||
FORM_BUBBLE: {
|
||||
SUBMIT: 'Einreichen',
|
||||
},
|
||||
},
|
||||
CONFIRM_EMAIL: 'Überprüfen...',
|
||||
SETTINGS: {
|
||||
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
DOWNLOAD: 'Download',
|
||||
UPLOADING: 'Uploading...',
|
||||
},
|
||||
FORM_BUBBLE: {
|
||||
SUBMIT: 'Submit',
|
||||
},
|
||||
},
|
||||
CONFIRM_EMAIL: 'Verifying...',
|
||||
SETTINGS: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { IFrameHelper } from '../sdk/IFrameHelper';
|
||||
import { onBubbleClick } from '../sdk/bubbleHelpers';
|
||||
|
||||
const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
const chatwootSettings = window.chatwootSettings || {};
|
||||
@@ -13,7 +12,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
websiteToken,
|
||||
|
||||
toggle() {
|
||||
onBubbleClick();
|
||||
IFrameHelper.events.toggleBubble();
|
||||
},
|
||||
|
||||
setUser(identifier, user) {
|
||||
@@ -39,7 +38,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
|
||||
reset() {
|
||||
if (window.$chatwoot.isOpen) {
|
||||
onBubbleClick();
|
||||
IFrameHelper.events.toggleBubble();
|
||||
}
|
||||
|
||||
Cookies.remove('cw_conversation');
|
||||
|
||||
@@ -88,6 +88,9 @@ export const IFrameHelper = {
|
||||
|
||||
toggleBubble: () => {
|
||||
onBubbleClick();
|
||||
if (window.$chatwoot.isOpen) {
|
||||
IFrameHelper.pushEvent('webwidget.triggered');
|
||||
}
|
||||
},
|
||||
},
|
||||
onLoad: ({ widget_color: widgetColor }) => {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
:key="action.uri"
|
||||
class="action-button button"
|
||||
:href="action.uri"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
>
|
||||
{{ action.text }}
|
||||
</a>
|
||||
@@ -44,11 +46,14 @@ export default {
|
||||
@import '~dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
max-height: 34px;
|
||||
margin-top: $space-smaller;
|
||||
align-items: center;
|
||||
border-radius: $space-micro;
|
||||
display: flex;
|
||||
font-weight: $font-weight-medium;
|
||||
justify-content: center;
|
||||
margin-top: $space-smaller;
|
||||
max-height: 34px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,7 +52,6 @@ export default {
|
||||
@import '~dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.card-message {
|
||||
@include border-normal;
|
||||
background: white;
|
||||
max-width: 220px;
|
||||
padding: $space-small;
|
||||
|
||||
114
app/javascript/shared/components/ChatForm.vue
Normal file
114
app/javascript/shared/components/ChatForm.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="form chat-bubble agent">
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div v-for="item in items" :key="item.key" class="form-block">
|
||||
<label>{{ item.label }}</label>
|
||||
<input
|
||||
v-if="item.type === 'email' || item.type === 'text'"
|
||||
v-model="formValues[item.name]"
|
||||
:type="item.type"
|
||||
:name="item.name"
|
||||
:placeholder="item.placeholder"
|
||||
:disabled="!!submittedValues.length"
|
||||
/>
|
||||
<textarea
|
||||
v-else-if="item.type === 'text_area'"
|
||||
v-model="formValues[item.name]"
|
||||
:name="item.name"
|
||||
:placeholder="item.placeholder"
|
||||
:disabled="!!submittedValues.length"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="!submittedValues.length"
|
||||
class="button small block"
|
||||
type="submit"
|
||||
:disabled="!isFormValid"
|
||||
>
|
||||
{{ $t('COMPONENTS.FORM_BUBBLE.SUBMIT') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
submittedValues: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formValues: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return this.items.reduce((acc, { name }) => {
|
||||
return !!this.formValues[name] && acc;
|
||||
}, true);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateFormValues();
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
if (!this.isFormValid) {
|
||||
return;
|
||||
}
|
||||
this.$emit('submit', this.formValues);
|
||||
},
|
||||
updateFormValues() {
|
||||
this.formValues = this.submittedValues.reduce((acc, obj) => {
|
||||
return {
|
||||
...acc,
|
||||
[obj.name]: obj.value,
|
||||
};
|
||||
}, {});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.form {
|
||||
padding: $space-normal;
|
||||
width: 100%;
|
||||
|
||||
.form-block {
|
||||
max-width: 100%;
|
||||
padding-bottom: $space-small;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: $font-weight-bold;
|
||||
padding: $space-smaller 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border-radius: $space-smaller;
|
||||
border: 1px solid $color-border;
|
||||
display: block;
|
||||
font-size: $font-size-default;
|
||||
line-height: 1.5;
|
||||
padding: $space-smaller $space-small;
|
||||
width: 90%;
|
||||
|
||||
&:disabled {
|
||||
background: $color-background-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<button class="option-button button" @click="onClick">
|
||||
<span v-if="isSelected" class="icon ion-checkmark-circled" />
|
||||
<span v-else class="icon ion-android-radio-button-off" />
|
||||
<span>{{ action.text }}</span>
|
||||
<span>{{ action.title }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
// Do postback here
|
||||
this.$emit('click', this.action);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
<h4 class="title">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<ul class="options" :class="{ 'has-selected': !!selected }">
|
||||
<ul
|
||||
v-if="!hideFields"
|
||||
class="options"
|
||||
:class="{ 'has-selected': !!selected }"
|
||||
>
|
||||
<chat-option
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:action="option"
|
||||
:is-selected="isSelected(option)"
|
||||
@click="onClick"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -36,11 +41,18 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hideFields: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isSelected(option) {
|
||||
return this.selected === option.id;
|
||||
},
|
||||
onClick(selectedOption) {
|
||||
this.$emit('click', selectedOption);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -59,7 +71,6 @@ export default {
|
||||
@import '~dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.options-message {
|
||||
@include border-normal;
|
||||
background: white;
|
||||
width: 60%;
|
||||
max-width: 17rem;
|
||||
|
||||
@@ -6,5 +6,12 @@ export default {
|
||||
const messageFormatter = new MessageFormatter(message);
|
||||
return messageFormatter.formattedMessage;
|
||||
},
|
||||
truncateMessage(description = '') {
|
||||
if (description.length < 100) {
|
||||
return description;
|
||||
}
|
||||
|
||||
return `${description.slice(0, 97)}...`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ export default {
|
||||
window.refererURL = message.refererURL;
|
||||
} else if (message.event === 'toggle-close-button') {
|
||||
this.isMobile = message.showClose;
|
||||
} else if (message.event === 'push-event') {
|
||||
this.$store.dispatch('events/create', { name: message.eventName });
|
||||
} else if (message.event === 'set-label') {
|
||||
this.$store.dispatch('conversationLabels/create', message.label);
|
||||
} else if (message.event === 'remove-label') {
|
||||
|
||||
@@ -30,7 +30,7 @@ const getConversation = ({ before }) => ({
|
||||
params: { before },
|
||||
});
|
||||
|
||||
const updateContact = id => ({
|
||||
const updateMessage = id => ({
|
||||
url: `/api/v1/widget/messages/${id}${window.location.search}`,
|
||||
});
|
||||
|
||||
@@ -45,6 +45,6 @@ export default {
|
||||
sendMessage,
|
||||
sendAttachmnet,
|
||||
getConversation,
|
||||
updateContact,
|
||||
updateMessage,
|
||||
getAvailableAgents,
|
||||
};
|
||||
|
||||
7
app/javascript/widget/api/events.js
Normal file
7
app/javascript/widget/api/events.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
export default {
|
||||
create(name) {
|
||||
return API.post(`/api/v1/widget/events${window.location.search}`, { name });
|
||||
},
|
||||
};
|
||||
@@ -2,10 +2,11 @@ import authEndPoint from 'widget/api/endPoints';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
export default {
|
||||
update: ({ messageId, email }) => {
|
||||
const urlData = authEndPoint.updateContact(messageId);
|
||||
update: ({ messageId, email, values }) => {
|
||||
const urlData = authEndPoint.updateMessage(messageId);
|
||||
return API.patch(urlData.url, {
|
||||
contact: { email },
|
||||
message: { submitted_values: values },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -54,4 +54,8 @@ $button-border-width: 1px;
|
||||
height: $space-larger;
|
||||
padding: $space-small $space-medium;
|
||||
}
|
||||
|
||||
&.block {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
<template>
|
||||
<div class="agent-message">
|
||||
<div class="avatar-wrap">
|
||||
<thumbnail
|
||||
v-if="message.showAvatar"
|
||||
:src="avatarUrl"
|
||||
size="24px"
|
||||
:username="agentName"
|
||||
/>
|
||||
</div>
|
||||
<div class="message-wrap">
|
||||
<AgentMessageBubble
|
||||
v-if="showTextBubble"
|
||||
:content-type="contentType"
|
||||
:message-content-attributes="messageContentAttributes"
|
||||
:message-id="message.id"
|
||||
:message-type="messageType"
|
||||
:message="message.content"
|
||||
/>
|
||||
<div v-if="hasAttachment" class="chat-bubble has-attachment agent">
|
||||
<file-bubble
|
||||
v-if="message.attachment && message.attachment.file_type !== 'image'"
|
||||
:url="message.attachment.data_url"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
:url="message.attachment.data_url"
|
||||
:thumb="message.attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
<div class="agent-bubble">
|
||||
<div class="agent-message">
|
||||
<div class="avatar-wrap">
|
||||
<thumbnail
|
||||
v-if="message.showAvatar || hasRecordedResponse"
|
||||
:src="avatarUrl"
|
||||
size="24px"
|
||||
:username="agentName"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="message.showAvatar" class="agent-name">
|
||||
{{ agentName }}
|
||||
</p>
|
||||
<div class="message-wrap">
|
||||
<AgentMessageBubble
|
||||
v-if="showTextBubble && shouldDisplayAgentMessage"
|
||||
:content-type="contentType"
|
||||
:message-content-attributes="messageContentAttributes"
|
||||
:message-id="message.id"
|
||||
:message-type="messageType"
|
||||
:message="message.content"
|
||||
/>
|
||||
<div v-if="hasAttachment" class="chat-bubble has-attachment agent">
|
||||
<file-bubble
|
||||
v-if="
|
||||
message.attachment && message.attachment.file_type !== 'image'
|
||||
"
|
||||
:url="message.attachment.data_url"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
:url="message.attachment.data_url"
|
||||
:thumb="message.attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
|
||||
{{ agentName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UserMessage v-if="hasRecordedResponse" :message="responseMessage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserMessage from 'widget/components/UserMessage';
|
||||
import AgentMessageBubble from 'widget/components/AgentMessageBubble';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import ImageBubble from 'widget/components/ImageBubble';
|
||||
@@ -48,8 +55,9 @@ export default {
|
||||
name: 'AgentMessage',
|
||||
components: {
|
||||
AgentMessageBubble,
|
||||
Thumbnail,
|
||||
ImageBubble,
|
||||
Thumbnail,
|
||||
UserMessage,
|
||||
FileBubble,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
@@ -60,12 +68,22 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldDisplayAgentMessage() {
|
||||
if (
|
||||
this.contentType === 'input_select' &&
|
||||
this.messageContentAttributes.submitted_values &&
|
||||
!this.message.content
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
hasAttachment() {
|
||||
return !!this.message.attachment;
|
||||
},
|
||||
showTextBubble() {
|
||||
const { message } = this;
|
||||
return !!message.content;
|
||||
return !message.attachment;
|
||||
},
|
||||
readableTime() {
|
||||
const { created_at: createdAt = '' } = this.message;
|
||||
@@ -88,25 +106,59 @@ export default {
|
||||
return 'Bot';
|
||||
}
|
||||
|
||||
return this.message.sender ? this.message.sender.name : '';
|
||||
return this.message.sender ? this.message.sender.name : 'Bot';
|
||||
},
|
||||
avatarUrl() {
|
||||
// eslint-disable-next-line
|
||||
const BotImage = require('dashboard/assets/images/chatwoot_bot.png')
|
||||
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
|
||||
// eslint-disable-next-line
|
||||
return require('dashboard/assets/images/chatwoot_bot.png');
|
||||
return BotImage;
|
||||
}
|
||||
|
||||
return this.message.sender ? this.message.sender.avatar_url : '';
|
||||
return this.message.sender ? this.message.sender.avatar_url : BotImage;
|
||||
},
|
||||
hasRecordedResponse() {
|
||||
return (
|
||||
this.messageContentAttributes.submitted_email ||
|
||||
(this.messageContentAttributes.submitted_values &&
|
||||
this.contentType !== 'form')
|
||||
);
|
||||
},
|
||||
responseMessage() {
|
||||
if (this.messageContentAttributes.submitted_email) {
|
||||
return { content: this.messageContentAttributes.submitted_email };
|
||||
}
|
||||
|
||||
if (this.messageContentAttributes.submitted_values) {
|
||||
if (this.contentType === 'input_select') {
|
||||
const [
|
||||
selectionOption = {},
|
||||
] = this.messageContentAttributes.submitted_values;
|
||||
return { content: selectionOption.title || selectionOption.value };
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.conversation-wrap {
|
||||
.agent-bubble {
|
||||
margin-bottom: $space-micro;
|
||||
& + .agent-bubble {
|
||||
.agent-message {
|
||||
.chat-bubble {
|
||||
border-top-left-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agent-message {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
@@ -115,6 +167,10 @@ export default {
|
||||
margin: 0 0 $space-micro $space-small;
|
||||
max-width: 88%;
|
||||
|
||||
& + .user-message {
|
||||
margin-top: $space-one;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
height: $space-medium;
|
||||
width: $space-medium;
|
||||
@@ -147,22 +203,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.conversation-wrap {
|
||||
.agent-message {
|
||||
+ .agent-message {
|
||||
margin-bottom: $space-micro;
|
||||
|
||||
.chat-bubble {
|
||||
border-top-left-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
+ .user-message {
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,64 @@
|
||||
<template>
|
||||
<div class="chat-bubble agent">
|
||||
<span v-html="formatMessage(message)"></span>
|
||||
<email-input
|
||||
v-if="shouldShowInput"
|
||||
:message-id="messageId"
|
||||
:message-content-attributes="messageContentAttributes"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
v-if="!isCards && !isOptions && !isForm && !isArticle"
|
||||
class="chat-bubble agent"
|
||||
>
|
||||
<span v-html="formatMessage(message)"></span>
|
||||
<email-input
|
||||
v-if="isTemplateEmail"
|
||||
:message-id="messageId"
|
||||
:message-content-attributes="messageContentAttributes"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isOptions">
|
||||
<chat-options
|
||||
:title="message"
|
||||
:options="messageContentAttributes.items"
|
||||
:hide-fields="!!messageContentAttributes.submitted_values"
|
||||
@click="onOptionSelect"
|
||||
>
|
||||
</chat-options>
|
||||
</div>
|
||||
<chat-form
|
||||
v-if="isForm"
|
||||
:items="messageContentAttributes.items"
|
||||
:submitted-values="messageContentAttributes.submitted_values"
|
||||
@submit="onFormSubmit"
|
||||
>
|
||||
</chat-form>
|
||||
<div v-if="isCards">
|
||||
<chat-card
|
||||
v-for="item in messageContentAttributes.items"
|
||||
:key="item.title"
|
||||
:media-url="item.media_url"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:actions="item.actions"
|
||||
>
|
||||
</chat-card>
|
||||
</div>
|
||||
<div v-if="isArticle">
|
||||
<chat-article :items="messageContentAttributes.items"></chat-article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import ChatCard from 'shared/components/ChatCard';
|
||||
import ChatForm from 'shared/components/ChatForm';
|
||||
import ChatOptions from 'shared/components/ChatOptions';
|
||||
import ChatArticle from './template/Article';
|
||||
import EmailInput from './template/EmailInput';
|
||||
|
||||
export default {
|
||||
name: 'AgentMessageBubble',
|
||||
components: {
|
||||
ChatArticle,
|
||||
ChatCard,
|
||||
ChatForm,
|
||||
ChatOptions,
|
||||
EmailInput,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
@@ -30,8 +73,44 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldShowInput() {
|
||||
return this.contentType === 'input_email' && this.messageType === 3;
|
||||
isTemplate() {
|
||||
return this.messageType === 3;
|
||||
},
|
||||
isTemplateEmail() {
|
||||
return this.contentType === 'input_email';
|
||||
},
|
||||
isCards() {
|
||||
return this.contentType === 'cards';
|
||||
},
|
||||
isOptions() {
|
||||
return this.contentType === 'input_select';
|
||||
},
|
||||
isForm() {
|
||||
return this.contentType === 'form';
|
||||
},
|
||||
isArticle() {
|
||||
return this.contentType === 'article';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onResponse(messageResponse) {
|
||||
this.$store.dispatch('message/update', messageResponse);
|
||||
},
|
||||
onOptionSelect(selectedOption) {
|
||||
this.onResponse({
|
||||
submittedValues: [selectedOption],
|
||||
messageId: this.messageId,
|
||||
});
|
||||
},
|
||||
onFormSubmit(formValues) {
|
||||
const formValuesAsArray = Object.keys(formValues).map(key => ({
|
||||
name: key,
|
||||
value: formValues[key],
|
||||
}));
|
||||
this.onResponse({
|
||||
submittedValues: formValuesAsArray,
|
||||
messageId: this.messageId,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
padding: $space-small $space-normal;
|
||||
text-align: left;
|
||||
|
||||
a {
|
||||
> a {
|
||||
color: $color-primary;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
&.user {
|
||||
border-bottom-right-radius: $space-smaller;
|
||||
|
||||
a {
|
||||
> a {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
62
app/javascript/widget/components/template/Article.vue
Normal file
62
app/javascript/widget/components/template/Article.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div v-if="!!items.length" class="chat-bubble agent">
|
||||
<div v-for="item in items" :key="item.link" class="article-item">
|
||||
<a :href="item.link" target="_blank" rel="noopener noreferrer nofollow">
|
||||
<span class="title">
|
||||
<i class="ion-link icon"></i>{{ item.title }}
|
||||
</span>
|
||||
<span class="description">{{ truncateMessage(item.description) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
|
||||
export default {
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.article-item {
|
||||
border-bottom: 1px solid $color-border;
|
||||
font-size: $font-size-default;
|
||||
padding: $space-small 0;
|
||||
|
||||
a {
|
||||
color: $color-body;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $color-woot;
|
||||
display: block;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
.icon {
|
||||
color: $color-body;
|
||||
font-size: $font-size-medium;
|
||||
padding-right: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
display: block;
|
||||
margin-top: $space-smaller;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,9 +21,6 @@
|
||||
<spinner v-else />
|
||||
</button>
|
||||
</form>
|
||||
<span v-else>
|
||||
<i>{{ messageContentAttributes.submitted_email }}</i>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -71,7 +68,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$store.dispatch('message/updateContactAttributes', {
|
||||
this.$store.dispatch('message/update', {
|
||||
email: this.email,
|
||||
messageId: this.messageId,
|
||||
});
|
||||
|
||||
@@ -14,6 +14,9 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
}
|
||||
|
||||
export const refreshActionCableConnector = pubsubToken => {
|
||||
if (!pubsubToken) {
|
||||
return;
|
||||
}
|
||||
window.chatwootPubsubToken = pubsubToken;
|
||||
window.actionCable.disconnect();
|
||||
window.actionCable = new ActionCableConnector(
|
||||
|
||||
@@ -4,5 +4,8 @@ export default {
|
||||
DOWNLOAD: 'Download',
|
||||
UPLOADING: 'Uploading...',
|
||||
},
|
||||
FORM_BUBBLE: {
|
||||
SUBMIT: 'Submit',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import appConfig from 'widget/store/modules/appConfig';
|
||||
import contacts from 'widget/store/modules/contacts';
|
||||
import conversation from 'widget/store/modules/conversation';
|
||||
import conversationLabels from 'widget/store/modules/conversationLabels';
|
||||
import events from 'widget/store/modules/events';
|
||||
import message from 'widget/store/modules/message';
|
||||
|
||||
Vue.use(Vuex);
|
||||
@@ -13,9 +14,10 @@ export default new Vuex.Store({
|
||||
modules: {
|
||||
agent,
|
||||
appConfig,
|
||||
message,
|
||||
contacts,
|
||||
conversation,
|
||||
conversationLabels,
|
||||
events,
|
||||
message,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -188,7 +188,10 @@ export const mutations = {
|
||||
updateMessage($state, { id, content_attributes }) {
|
||||
$state.conversations[id] = {
|
||||
...$state.conversations[id],
|
||||
content_attributes,
|
||||
content_attributes: {
|
||||
...($state.conversations[id].content_attributes || {}),
|
||||
...content_attributes,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
19
app/javascript/widget/store/modules/events.js
Normal file
19
app/javascript/widget/store/modules/events.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import events from 'widget/api/events';
|
||||
|
||||
const actions = {
|
||||
create: async (_, { name }) => {
|
||||
try {
|
||||
await events.create(name);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {},
|
||||
getters: {},
|
||||
actions,
|
||||
mutations: {},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import MessageAPI from 'widget/api/message';
|
||||
import MessageAPI from '../../api/message';
|
||||
import { refreshActionCableConnector } from '../../helpers/actionCable';
|
||||
|
||||
const state = {
|
||||
@@ -12,19 +12,24 @@ const getters = {
|
||||
};
|
||||
|
||||
const actions = {
|
||||
updateContactAttributes: async ({ commit }, { email, messageId }) => {
|
||||
update: async ({ commit }, { email, messageId, submittedValues }) => {
|
||||
commit('toggleUpdateStatus', true);
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
contact: { pubsub_token: pubsubToken },
|
||||
},
|
||||
} = await MessageAPI.update({ email, messageId });
|
||||
data: { contact: { pubsub_token: pubsubToken } = {} },
|
||||
} = await MessageAPI.update({
|
||||
email,
|
||||
messageId,
|
||||
values: submittedValues,
|
||||
});
|
||||
commit(
|
||||
'conversation/updateMessage',
|
||||
{
|
||||
id: messageId,
|
||||
content_attributes: { submitted_email: email },
|
||||
content_attributes: {
|
||||
submitted_email: email,
|
||||
submitted_values: email ? null : submittedValues,
|
||||
},
|
||||
},
|
||||
{ root: true }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user