feat: Add a pre-chat form on widget (#1769)
This commit is contained in:
parent
5f2bf7dfd2
commit
037ffc7419
31 changed files with 604 additions and 200 deletions
|
@ -20,11 +20,7 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_token_params
|
def auth_token_params
|
||||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers[header_name]).decode_token
|
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||||
end
|
|
||||||
|
|
||||||
def header_name
|
|
||||||
'X-Auth-Token'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_web_widget
|
def set_web_widget
|
||||||
|
@ -39,6 +35,50 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
@contact = @contact_inbox.contact
|
@contact = @contact_inbox.contact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_conversation
|
||||||
|
::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
@inbox ||= ::Inbox.find_by(id: auth_token_params[:inbox_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
# FIXME: typo referrer in additional attributes, will probably require a migration.
|
||||||
|
{
|
||||||
|
account_id: inbox.account_id,
|
||||||
|
inbox_id: inbox.id,
|
||||||
|
contact_id: @contact.id,
|
||||||
|
contact_inbox_id: @contact_inbox.id,
|
||||||
|
additional_attributes: {
|
||||||
|
browser: browser_params,
|
||||||
|
referer: permitted_params[:message][:referer_url],
|
||||||
|
initiated_at: timestamp_params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_contact(email)
|
||||||
|
contact_with_email = @current_account.contacts.find_by(email: email)
|
||||||
|
if contact_with_email
|
||||||
|
@contact = ::ContactMergeAction.new(
|
||||||
|
account: @current_account,
|
||||||
|
base_contact: contact_with_email,
|
||||||
|
mergee_contact: @contact
|
||||||
|
).perform
|
||||||
|
else
|
||||||
|
@contact.update!(email: email, name: contact_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_email
|
||||||
|
permitted_params[:contact][:email].downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact_name
|
||||||
|
params[:contact][:name] || contact_email.split('@')[0]
|
||||||
|
end
|
||||||
|
|
||||||
def browser_params
|
def browser_params
|
||||||
{
|
{
|
||||||
browser_name: browser.name,
|
browser_name: browser.name,
|
||||||
|
@ -48,4 +88,19 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
platform_version: browser.platform.version
|
platform_version: browser.platform.version
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def timestamp_params
|
||||||
|
{ timestamp: permitted_params[:message][:timestamp] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_params
|
||||||
|
{
|
||||||
|
account_id: conversation.account_id,
|
||||||
|
sender: @contact,
|
||||||
|
content: permitted_params[:message][:content],
|
||||||
|
inbox_id: conversation.inbox_id,
|
||||||
|
echo_id: permitted_params[:message][:echo_id],
|
||||||
|
message_type: :incoming
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,14 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
update_contact(contact_email) if @contact.email.blank?
|
||||||
|
@conversation = create_conversation
|
||||||
|
conversation.messages.create(message_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
head :ok && return if conversation.nil?
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
|
@ -43,6 +51,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :typing_status, :website_token, :email)
|
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,44 +39,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_conversation
|
def set_conversation
|
||||||
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
@conversation = create_conversation if conversation.nil?
|
||||||
end
|
|
||||||
|
|
||||||
def message_params
|
|
||||||
{
|
|
||||||
account_id: conversation.account_id,
|
|
||||||
sender: @contact,
|
|
||||||
content: permitted_params[:message][:content],
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
echo_id: permitted_params[:message][:echo_id],
|
|
||||||
message_type: :incoming
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
# FIXME: typo referrer in additional attributes
|
|
||||||
# will probably require a migration.
|
|
||||||
{
|
|
||||||
account_id: inbox.account_id,
|
|
||||||
inbox_id: inbox.id,
|
|
||||||
contact_id: @contact.id,
|
|
||||||
contact_inbox_id: @contact_inbox.id,
|
|
||||||
additional_attributes: {
|
|
||||||
browser: browser_params,
|
|
||||||
referer: permitted_params[:message][:referer_url],
|
|
||||||
initiated_at: timestamp_params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def timestamp_params
|
|
||||||
{
|
|
||||||
timestamp: permitted_params[:message][:timestamp]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def inbox
|
|
||||||
@inbox ||= ::Inbox.find_by(id: auth_token_params[:inbox_id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_finder_params
|
def message_finder_params
|
||||||
|
@ -90,36 +53,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
@message_finder ||= MessageFinder.new(conversation, message_finder_params)
|
@message_finder ||= MessageFinder.new(conversation, message_finder_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_contact(email)
|
|
||||||
contact_with_email = @current_account.contacts.find_by(email: email)
|
|
||||||
if contact_with_email
|
|
||||||
@contact = ::ContactMergeAction.new(
|
|
||||||
account: @current_account,
|
|
||||||
base_contact: contact_with_email,
|
|
||||||
mergee_contact: @contact
|
|
||||||
).perform
|
|
||||||
else
|
|
||||||
@contact.update!(
|
|
||||||
email: email,
|
|
||||||
name: contact_name
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def contact_email
|
|
||||||
permitted_params[:contact][:email].downcase
|
|
||||||
end
|
|
||||||
|
|
||||||
def contact_name
|
|
||||||
contact_email.split('@')[0]
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_update_params
|
def message_update_params
|
||||||
params.permit(message: [{ submitted_values: [:name, :title, :value] }])
|
params.permit(message: [{ submitted_values: [:name, :title, :value] }])
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp, :echo_id])
|
params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_message
|
def set_message
|
||||||
|
|
|
@ -2,12 +2,7 @@
|
||||||
<router
|
<router
|
||||||
:show-unread-view="showUnreadView"
|
:show-unread-view="showUnreadView"
|
||||||
:is-mobile="isMobile"
|
:is-mobile="isMobile"
|
||||||
:grouped-messages="groupedMessages"
|
|
||||||
:unread-messages="unreadMessages"
|
|
||||||
:conversation-size="conversationSize"
|
|
||||||
:available-agents="availableAgents"
|
|
||||||
:has-fetched="hasFetched"
|
:has-fetched="hasFetched"
|
||||||
:conversation-attributes="conversationAttributes"
|
|
||||||
:unread-message-count="unreadMessageCount"
|
:unread-message-count="unreadMessageCount"
|
||||||
:is-left-aligned="isLeftAligned"
|
:is-left-aligned="isLeftAligned"
|
||||||
:hide-message-bubble="hideMessageBubble"
|
:hide-message-bubble="hideMessageBubble"
|
||||||
|
@ -40,12 +35,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
groupedMessages: 'conversation/getGroupedConversation',
|
|
||||||
unreadMessages: 'conversation/getUnreadTextMessages',
|
|
||||||
conversationSize: 'conversation/getConversationSize',
|
|
||||||
availableAgents: 'agent/availableAgents',
|
|
||||||
hasFetched: 'agent/getHasFetched',
|
hasFetched: 'agent/getHasFetched',
|
||||||
conversationAttributes: 'conversationAttributes/getConversationParams',
|
|
||||||
unreadMessageCount: 'conversation/getUnreadMessageCount',
|
unreadMessageCount: 'conversation/getUnreadMessageCount',
|
||||||
}),
|
}),
|
||||||
isLeftAligned() {
|
isLeftAligned() {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import endPoints from 'widget/api/endPoints';
|
import endPoints from 'widget/api/endPoints';
|
||||||
import { API } from 'widget/helpers/axios';
|
import { API } from 'widget/helpers/axios';
|
||||||
|
|
||||||
|
const createConversationAPI = async content => {
|
||||||
|
const urlData = endPoints.createConversation(content);
|
||||||
|
const result = await API.post(urlData.url, urlData.params);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const sendMessageAPI = async content => {
|
const sendMessageAPI = async content => {
|
||||||
const urlData = endPoints.sendMessage(content);
|
const urlData = endPoints.sendMessage(content);
|
||||||
const result = await API.post(urlData.url, urlData.params);
|
const result = await API.post(urlData.url, urlData.params);
|
||||||
|
@ -38,6 +44,7 @@ const setUserLastSeenAt = async ({ lastSeen }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
createConversationAPI,
|
||||||
sendMessageAPI,
|
sendMessageAPI,
|
||||||
getConversationAPI,
|
getConversationAPI,
|
||||||
getMessagesAPI,
|
getMessagesAPI,
|
||||||
|
|
|
@ -1,5 +1,24 @@
|
||||||
import { buildSearchParamsWithLocale } from '../helpers/urlParamsHelper';
|
import { buildSearchParamsWithLocale } from '../helpers/urlParamsHelper';
|
||||||
|
|
||||||
|
const createConversation = params => {
|
||||||
|
const referrerURL = window.referrerURL || '';
|
||||||
|
const search = buildSearchParamsWithLocale(window.location.search);
|
||||||
|
return {
|
||||||
|
url: `/api/v1/widget/conversations${search}`,
|
||||||
|
params: {
|
||||||
|
contact: {
|
||||||
|
name: params.fullName,
|
||||||
|
email: params.emailAddress,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
content: params.message,
|
||||||
|
timestamp: new Date().toString(),
|
||||||
|
referer_url: referrerURL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const sendMessage = content => {
|
const sendMessage = content => {
|
||||||
const referrerURL = window.referrerURL || '';
|
const referrerURL = window.referrerURL || '';
|
||||||
const search = buildSearchParamsWithLocale(window.location.search);
|
const search = buildSearchParamsWithLocale(window.location.search);
|
||||||
|
@ -47,6 +66,7 @@ const getAvailableAgents = token => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
createConversation,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendAttachment,
|
sendAttachment,
|
||||||
getConversation,
|
getConversation,
|
||||||
|
|
|
@ -75,8 +75,6 @@ export default {
|
||||||
padding: $space-two $space-medium;
|
padding: $space-two $space-medium;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: white;
|
|
||||||
@include shadow-large;
|
|
||||||
|
|
||||||
.header-branding {
|
.header-branding {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="header-expanded py-8 px-6 bg-white relative box-border w-full">
|
<header class="header-expanded bg-white py-8 px-6 relative box-border w-full">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<img v-if="avatarUrl" class="logo" :src="avatarUrl" />
|
<img v-if="avatarUrl" class="logo" :src="avatarUrl" />
|
||||||
<header-actions :show-popout-button="showPopoutButton" />
|
<header-actions :show-popout-button="showPopoutButton" />
|
||||||
|
@ -50,8 +50,6 @@ export default {
|
||||||
@import '~widget/assets/scss/mixins.scss';
|
@import '~widget/assets/scss/mixins.scss';
|
||||||
|
|
||||||
.header-expanded {
|
.header-expanded {
|
||||||
@include shadow-large;
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
|
59
app/javascript/widget/components/Form/Input.vue
Normal file
59
app/javascript/widget/components/Form/Input.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<label class="block">
|
||||||
|
<div
|
||||||
|
v-if="label"
|
||||||
|
class="mb-2 text-xs font-medium"
|
||||||
|
:class="{
|
||||||
|
'text-black-800': !error,
|
||||||
|
'text-red-400': error,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
:type="type"
|
||||||
|
class="border rounded w-full py-2 px-3 text-slate-700 leading-tight outline-none"
|
||||||
|
:class="{
|
||||||
|
'border-black-200 hover:border-black-300 focus:border-black-300': !error,
|
||||||
|
'border-red-200 hover:border-red-300 focus:border-red-300': error,
|
||||||
|
}"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:value="value"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(event) {
|
||||||
|
this.$emit('input', event.target.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
63
app/javascript/widget/components/Form/TextArea.vue
Normal file
63
app/javascript/widget/components/Form/TextArea.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<label class="block">
|
||||||
|
<div
|
||||||
|
v-if="label"
|
||||||
|
class="mb-2 text-xs font-medium"
|
||||||
|
:class="{
|
||||||
|
'text-black-800': !error,
|
||||||
|
'text-red-400': error,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
class="resize-none border rounded w-full py-2 px-3 text-slate-700 leading-tight outline-none"
|
||||||
|
:class="{
|
||||||
|
'border-black-200 hover:border-black-300 focus:border-black-300': !error,
|
||||||
|
'border-red-200 hover:border-red-300 focus:border-red-300': error,
|
||||||
|
}"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:value="value"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(event) {
|
||||||
|
this.$emit('input', event.target.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
textarea {
|
||||||
|
min-height: 8rem;
|
||||||
|
}
|
||||||
|
</style>
|
117
app/javascript/widget/components/PreChat/Form.vue
Normal file
117
app/javascript/widget/components/PreChat/Form.vue
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="flex flex-1 flex-col p-6 overflow-y-scroll"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<div v-if="options.preChatMessage" class="text-black-800 text-sm leading-5">
|
||||||
|
{{ options.preChatMessage }}
|
||||||
|
</div>
|
||||||
|
<form-input
|
||||||
|
v-if="options.requireEmail"
|
||||||
|
v-model="fullName"
|
||||||
|
class="mt-5"
|
||||||
|
:label="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.LABEL')"
|
||||||
|
:placeholder="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.PLACEHOLDER')"
|
||||||
|
type="text"
|
||||||
|
:error="
|
||||||
|
$v.fullName.$error ? $t('PRE_CHAT_FORM.FIELDS.FULL_NAME.ERROR') : ''
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<form-input
|
||||||
|
v-if="options.requireEmail"
|
||||||
|
v-model="emailAddress"
|
||||||
|
class="mt-5"
|
||||||
|
:label="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.LABEL')"
|
||||||
|
:placeholder="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||||
|
type="email"
|
||||||
|
:error="
|
||||||
|
$v.emailAddress.$error
|
||||||
|
? $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.ERROR')
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<form-text-area
|
||||||
|
v-model="message"
|
||||||
|
class="my-5"
|
||||||
|
:label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')"
|
||||||
|
:placeholder="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.PLACEHOLDER')"
|
||||||
|
:error="$v.message.$error ? $t('PRE_CHAT_FORM.FIELDS.MESSAGE.ERROR') : ''"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
class="font-medium"
|
||||||
|
block
|
||||||
|
:bg-color="widgetColor"
|
||||||
|
:text-color="textColor"
|
||||||
|
:disabled="isCreating"
|
||||||
|
>
|
||||||
|
<spinner v-if="isCreating" class="p-0" />
|
||||||
|
{{ $t('START_CONVERSATION') }}
|
||||||
|
</woot-button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import WootButton from 'shared/components/Button';
|
||||||
|
import FormInput from '../Form/Input';
|
||||||
|
import FormTextArea from '../Form/TextArea';
|
||||||
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { getContrastingTextColor } from 'shared/helpers/ColorHelper';
|
||||||
|
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
FormInput,
|
||||||
|
FormTextArea,
|
||||||
|
WootButton,
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
fullName: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
emailAddress: {
|
||||||
|
required,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
required,
|
||||||
|
minLength: minLength(10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
fullName: '',
|
||||||
|
emailAddress: '',
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
isCreating: 'conversation/getIsCreating',
|
||||||
|
}),
|
||||||
|
textColor() {
|
||||||
|
return getContrastingTextColor(this.widgetColor);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSubmit() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('conversation/createConversation', {
|
||||||
|
fullName: this.fullName,
|
||||||
|
emailAddress: this.emailAddress,
|
||||||
|
message: this.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -29,5 +29,24 @@
|
||||||
"EMAIL_PLACEHOLDER": "Please enter your email",
|
"EMAIL_PLACEHOLDER": "Please enter your email",
|
||||||
"CHAT_PLACEHOLDER": "Type your message",
|
"CHAT_PLACEHOLDER": "Type your message",
|
||||||
"TODAY": "Today",
|
"TODAY": "Today",
|
||||||
"YESTERDAY": "Yesterday"
|
"YESTERDAY": "Yesterday",
|
||||||
|
"PRE_CHAT_FORM": {
|
||||||
|
"FIELDS": {
|
||||||
|
"FULL_NAME": {
|
||||||
|
"LABEL": "Full Name",
|
||||||
|
"PLACEHOLDER": "Please enter your full name",
|
||||||
|
"ERROR": "Full Name is required"
|
||||||
|
},
|
||||||
|
"EMAIL_ADDRESS": {
|
||||||
|
"LABEL": "Email Address",
|
||||||
|
"PLACEHOLDER": "Please enter your email address",
|
||||||
|
"ERROR": "Invalid email address"
|
||||||
|
},
|
||||||
|
"MESSAGE": {
|
||||||
|
"LABEL": "Message",
|
||||||
|
"PLACEHOLDER": "Please enter your message",
|
||||||
|
"ERROR": "Message too short"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,16 @@ export default {
|
||||||
replyTime() {
|
replyTime() {
|
||||||
return window.chatwootWebChannel.replyTime;
|
return window.chatwootWebChannel.replyTime;
|
||||||
},
|
},
|
||||||
|
preChatFormEnabled() {
|
||||||
|
return window.chatwootWebChannel.preChatFormEnabled;
|
||||||
|
},
|
||||||
|
preChatFormOptions() {
|
||||||
|
const options = window.chatwootWebChannel.preChatFormOptions || {};
|
||||||
|
return {
|
||||||
|
requireEmail: options.require_email,
|
||||||
|
preChatMessage: options.pre_chat_message,
|
||||||
|
};
|
||||||
|
},
|
||||||
replyTimeStatus() {
|
replyTimeStatus() {
|
||||||
switch (this.replyTime) {
|
switch (this.replyTime) {
|
||||||
case 'in_a_few_minutes':
|
case 'in_a_few_minutes':
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
import {
|
import {
|
||||||
|
createConversationAPI,
|
||||||
sendMessageAPI,
|
sendMessageAPI,
|
||||||
getMessagesAPI,
|
getMessagesAPI,
|
||||||
sendAttachmentAPI,
|
sendAttachmentAPI,
|
||||||
toggleTyping,
|
toggleTyping,
|
||||||
setUserLastSeenAt,
|
setUserLastSeenAt,
|
||||||
} from 'widget/api/conversation';
|
} from 'widget/api/conversation';
|
||||||
|
import { refreshActionCableConnector } from '../../../helpers/actionCable';
|
||||||
|
|
||||||
import { createTemporaryMessage, onNewMessageCreated } from './helpers';
|
import { createTemporaryMessage, onNewMessageCreated } from './helpers';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
createConversation: async ({ commit }, params) => {
|
||||||
|
commit('setConversationUIFlag', { isCreating: true });
|
||||||
|
try {
|
||||||
|
const { data } = await createConversationAPI(params);
|
||||||
|
const {
|
||||||
|
contact: { pubsub_token: pubsubToken },
|
||||||
|
messages,
|
||||||
|
} = data;
|
||||||
|
const [message = {}] = messages;
|
||||||
|
commit('pushMessageToConversation', message);
|
||||||
|
refreshActionCableConnector(pubsubToken);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit('setConversationUIFlag', { isCreating: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
sendMessage: async ({ commit }, params) => {
|
sendMessage: async ({ commit }, params) => {
|
||||||
const { content } = params;
|
const { content } = params;
|
||||||
commit('pushMessageToConversation', createTemporaryMessage({ content }));
|
commit('pushMessageToConversation', createTemporaryMessage({ content }));
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { formatUnixDate } from 'shared/helpers/DateHelper';
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded,
|
getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded,
|
||||||
|
getIsCreating: _state => _state.uiFlags.isCreating,
|
||||||
getIsAgentTyping: _state => _state.uiFlags.isAgentTyping,
|
getIsAgentTyping: _state => _state.uiFlags.isAgentTyping,
|
||||||
getConversation: _state => _state.conversations,
|
getConversation: _state => _state.conversations,
|
||||||
getConversationSize: _state => Object.keys(_state.conversations).length,
|
getConversationSize: _state => Object.keys(_state.conversations).length,
|
||||||
|
|
|
@ -11,6 +11,7 @@ const state = {
|
||||||
allMessagesLoaded: false,
|
allMessagesLoaded: false,
|
||||||
isFetchingList: false,
|
isFetchingList: false,
|
||||||
isAgentTyping: false,
|
isAgentTyping: false,
|
||||||
|
isCreating: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,13 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setConversationUIFlag($state, uiFlags) {
|
||||||
|
$state.uiFlags = {
|
||||||
|
...$state.uiFlags,
|
||||||
|
...uiFlags,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
setConversationListLoading($state, status) {
|
setConversationListLoading($state, status) {
|
||||||
$state.uiFlags.isFetchingList = status;
|
$state.uiFlags.isFetchingList = status;
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,6 +28,43 @@ describe('#actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#createConversation', () => {
|
||||||
|
it('sends correct mutations', async () => {
|
||||||
|
API.post.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
contact: { name: 'contact-name' },
|
||||||
|
messages: [{ id: 1, content: 'This is a test message' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let windowSpy = jest.spyOn(window, 'window', 'get');
|
||||||
|
windowSpy.mockImplementation(() => ({
|
||||||
|
WOOT_WIDGET: {
|
||||||
|
$root: {
|
||||||
|
$i18n: {
|
||||||
|
locale: 'el',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
search: '?param=1',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
await actions.createConversation(
|
||||||
|
{ commit },
|
||||||
|
{ contact: {}, message: 'This is a test message' }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
['setConversationUIFlag', { isCreating: true }],
|
||||||
|
[
|
||||||
|
'pushMessageToConversation',
|
||||||
|
{ id: 1, content: 'This is a test message' },
|
||||||
|
],
|
||||||
|
['setConversationUIFlag', { isCreating: false }],
|
||||||
|
]);
|
||||||
|
windowSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#updateMessage', () => {
|
describe('#updateMessage', () => {
|
||||||
it('sends correct mutations', () => {
|
it('sends correct mutations', () => {
|
||||||
actions.updateMessage({ commit }, { id: 1 });
|
actions.updateMessage({ commit }, { id: 1 });
|
||||||
|
|
|
@ -16,6 +16,11 @@ describe('#getters', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getIsCreating', () => {
|
||||||
|
const state = { uiFlags: { isCreating: true } };
|
||||||
|
expect(getters.getIsCreating(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('getConversationSize', () => {
|
it('getConversationSize', () => {
|
||||||
const state = {
|
const state = {
|
||||||
conversations: {
|
conversations: {
|
||||||
|
|
|
@ -73,6 +73,17 @@ describe('#mutations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#setConversationUIFlag', () => {
|
||||||
|
it('set uiFlags correctly', () => {
|
||||||
|
const state = { uiFlags: { isFetchingList: false } };
|
||||||
|
mutations.setConversationUIFlag(state, { isCreating: true });
|
||||||
|
expect(state.uiFlags).toEqual({
|
||||||
|
isFetchingList: false,
|
||||||
|
isCreating: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#setMessagesInConversation', () => {
|
describe('#setMessagesInConversation', () => {
|
||||||
it('sets allMessagesLoaded flag if payload is empty', () => {
|
it('sets allMessagesLoaded flag if payload is empty', () => {
|
||||||
const state = { uiFlags: { allMessagesLoaded: false } };
|
const state = { uiFlags: { allMessagesLoaded: false } };
|
||||||
|
|
|
@ -3,27 +3,30 @@
|
||||||
v-if="!conversationSize && isFetchingList"
|
v-if="!conversationSize && isFetchingList"
|
||||||
class="flex flex-1 items-center h-full bg-black-25 justify-center"
|
class="flex flex-1 items-center h-full bg-black-25 justify-center"
|
||||||
>
|
>
|
||||||
<spinner size=""></spinner>
|
<spinner size="" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="home">
|
<div v-else class="home">
|
||||||
<div class="header-wrap">
|
<div
|
||||||
|
class="header-wrap bg-white"
|
||||||
|
:class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }"
|
||||||
|
>
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition-all delay-200 duration-300 ease"
|
enter-active-class="transition-all delay-200 duration-300 ease"
|
||||||
leave-active-class="transition-all duration-200 ease-in"
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
enter-class="opacity-0 transform -translate-y-32"
|
enter-class="opacity-0 transform"
|
||||||
enter-to-class="opacity-100 transform translate-y-0"
|
enter-to-class="opacity-100 transform"
|
||||||
leave-class="opacity-100 transform translate-y-0"
|
leave-class="opacity-100 transform"
|
||||||
leave-to-class="opacity-0 transform -translate-y-32"
|
leave-to-class="opacity-0 transform"
|
||||||
>
|
>
|
||||||
<chat-header-expanded
|
<chat-header-expanded
|
||||||
v-if="!isOnMessageView"
|
v-if="!isHeaderCollapsed"
|
||||||
:intro-heading="introHeading"
|
:intro-heading="channelConfig.welcomeTitle"
|
||||||
:intro-body="introBody"
|
:intro-body="channelConfig.welcomeTagline"
|
||||||
:avatar-url="channelConfig.avatarUrl"
|
:avatar-url="channelConfig.avatarUrl"
|
||||||
:show-popout-button="showPopoutButton"
|
:show-popout-button="showPopoutButton"
|
||||||
/>
|
/>
|
||||||
<chat-header
|
<chat-header
|
||||||
v-if="isOnMessageView"
|
v-if="isHeaderCollapsed"
|
||||||
:title="channelConfig.websiteName"
|
:title="channelConfig.websiteName"
|
||||||
:avatar-url="channelConfig.avatarUrl"
|
:avatar-url="channelConfig.avatarUrl"
|
||||||
:show-popout-button="showPopoutButton"
|
:show-popout-button="showPopoutButton"
|
||||||
|
@ -31,21 +34,33 @@
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<conversation-wrap :grouped-messages="groupedMessages" />
|
<div class="flex flex-1 overflow-scroll">
|
||||||
|
<conversation-wrap
|
||||||
|
v-if="currentView === 'messageView'"
|
||||||
|
:grouped-messages="groupedMessages"
|
||||||
|
/>
|
||||||
|
<pre-chat-form
|
||||||
|
v-if="currentView === 'preChatFormView'"
|
||||||
|
:options="preChatFormOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="footer-wrap">
|
<div class="footer-wrap">
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition-all delay-300 duration-300 ease"
|
enter-active-class="transition-all delay-300 duration-300 ease"
|
||||||
leave-active-class="transition-all duration-200 ease-in"
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
enter-class="opacity-0 transform translate-y-32"
|
enter-class="opacity-0 transform"
|
||||||
enter-to-class="opacity-100 transform translate-y-0"
|
enter-to-class="opacity-100 transform translate-y-0"
|
||||||
leave-class="opacity-100 transform translate-y-0"
|
leave-class="opacity-100 transform translate-y-0"
|
||||||
leave-to-class="opacity-0 transform translate-y-32 "
|
leave-to-class="opacity-0 transform "
|
||||||
>
|
>
|
||||||
<div v-if="showInputTextArea && isOnMessageView" class="input-wrap">
|
<div
|
||||||
|
v-if="showInputTextArea && currentView === 'messageView'"
|
||||||
|
class="input-wrap"
|
||||||
|
>
|
||||||
<chat-footer />
|
<chat-footer />
|
||||||
</div>
|
</div>
|
||||||
<team-availability
|
<team-availability
|
||||||
v-if="!isOnMessageView"
|
v-if="currentView === 'cardView'"
|
||||||
:available-agents="availableAgents"
|
:available-agents="availableAgents"
|
||||||
@start-conversation="startConversation"
|
@start-conversation="startConversation"
|
||||||
/>
|
/>
|
||||||
|
@ -65,7 +80,7 @@ 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 { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import PreChatForm from '../components/PreChat/Form';
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
components: {
|
components: {
|
||||||
|
@ -74,45 +89,44 @@ export default {
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
ChatHeaderExpanded,
|
ChatHeaderExpanded,
|
||||||
ConversationWrap,
|
ConversationWrap,
|
||||||
|
PreChatForm,
|
||||||
Spinner,
|
Spinner,
|
||||||
TeamAvailability,
|
TeamAvailability,
|
||||||
},
|
},
|
||||||
mixins: [configMixin],
|
mixins: [configMixin],
|
||||||
props: {
|
props: {
|
||||||
groupedMessages: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
conversationSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
availableAgents: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
hasFetched: {
|
hasFetched: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
conversationAttributes: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
showPopoutButton: {
|
showPopoutButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { isOnCollapsedView: false };
|
||||||
showMessageView: false,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
availableAgents: 'agent/availableAgents',
|
||||||
|
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||||
|
conversationSize: 'conversation/getConversationSize',
|
||||||
|
groupedMessages: 'conversation/getGroupedConversation',
|
||||||
isFetchingList: 'conversation/getIsFetchingList',
|
isFetchingList: 'conversation/getIsFetchingList',
|
||||||
}),
|
}),
|
||||||
|
currentView() {
|
||||||
|
if (this.isHeaderCollapsed) {
|
||||||
|
if (this.conversationSize) {
|
||||||
|
return 'messageView';
|
||||||
|
}
|
||||||
|
if (this.preChatFormEnabled) {
|
||||||
|
return 'preChatFormView';
|
||||||
|
}
|
||||||
|
return 'messageView';
|
||||||
|
}
|
||||||
|
return 'cardView';
|
||||||
|
},
|
||||||
isOpen() {
|
isOpen() {
|
||||||
return this.conversationAttributes.status === 'open';
|
return this.conversationAttributes.status === 'open';
|
||||||
},
|
},
|
||||||
|
@ -125,31 +139,22 @@ export default {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
isOnMessageView() {
|
isHeaderCollapsed() {
|
||||||
if (this.hideWelcomeHeader) {
|
if (!this.hasIntroText || this.conversationSize) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.conversationSize === 0) {
|
|
||||||
return this.showMessageView;
|
return this.isOnCollapsedView;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
isHeaderExpanded() {
|
hasIntroText() {
|
||||||
return this.conversationSize === 0;
|
return (
|
||||||
},
|
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
|
||||||
introHeading() {
|
);
|
||||||
return this.channelConfig.welcomeTitle;
|
|
||||||
},
|
|
||||||
introBody() {
|
|
||||||
return this.channelConfig.welcomeTagline;
|
|
||||||
},
|
|
||||||
hideWelcomeHeader() {
|
|
||||||
return !(this.introHeading || this.introBody);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startConversation() {
|
startConversation() {
|
||||||
this.showMessageView = !this.showMessageView;
|
this.isOnCollapsedView = !this.isOnCollapsedView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -157,6 +162,7 @@ export default {
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~widget/assets/scss/variables';
|
@import '~widget/assets/scss/variables';
|
||||||
|
@import '~widget/assets/scss/mixins';
|
||||||
|
|
||||||
.home {
|
.home {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -168,9 +174,19 @@ export default {
|
||||||
background: $color-background;
|
background: $color-background;
|
||||||
|
|
||||||
.header-wrap {
|
.header-wrap {
|
||||||
|
border-radius: $space-normal $space-normal 0 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: $space-normal $space-normal $space-small $space-small;
|
transition: max-height 300ms;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
@include shadow-large;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
max-height: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
max-height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (min-device-width: 320px) and (max-device-width: 667px) {
|
@media only screen and (min-device-width: 320px) and (max-device-width: 667px) {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
|
@ -10,21 +10,13 @@
|
||||||
>
|
>
|
||||||
<home
|
<home
|
||||||
v-if="!showUnreadView"
|
v-if="!showUnreadView"
|
||||||
:grouped-messages="groupedMessages"
|
|
||||||
:conversation-size="conversationSize"
|
|
||||||
:available-agents="availableAgents"
|
|
||||||
:has-fetched="hasFetched"
|
:has-fetched="hasFetched"
|
||||||
:conversation-attributes="conversationAttributes"
|
|
||||||
:unread-message-count="unreadMessageCount"
|
:unread-message-count="unreadMessageCount"
|
||||||
:show-popout-button="showPopoutButton"
|
:show-popout-button="showPopoutButton"
|
||||||
/>
|
/>
|
||||||
<unread
|
<unread
|
||||||
v-else
|
v-else
|
||||||
:unread-messages="unreadMessages"
|
|
||||||
:conversation-size="conversationSize"
|
|
||||||
:available-agents="availableAgents"
|
|
||||||
:has-fetched="hasFetched"
|
:has-fetched="hasFetched"
|
||||||
:conversation-attributes="conversationAttributes"
|
|
||||||
:unread-message-count="unreadMessageCount"
|
:unread-message-count="unreadMessageCount"
|
||||||
:hide-message-bubble="hideMessageBubble"
|
:hide-message-bubble="hideMessageBubble"
|
||||||
/>
|
/>
|
||||||
|
@ -42,22 +34,6 @@ export default {
|
||||||
Unread,
|
Unread,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
groupedMessages: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
unreadMessages: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
conversationSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
availableAgents: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
hasFetched: {
|
hasFetched: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -78,10 +54,6 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
conversationAttributes: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
unreadMessageCount: {
|
unreadMessageCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
import { IFrameHelper } from 'widget/helpers/utils';
|
import { IFrameHelper } from 'widget/helpers/utils';
|
||||||
import AgentBubble from 'widget/components/AgentMessageBubble.vue';
|
import AgentBubble from 'widget/components/AgentMessageBubble.vue';
|
||||||
import configMixin from '../mixins/configMixin';
|
import configMixin from '../mixins/configMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Unread',
|
name: 'Unread',
|
||||||
|
@ -43,26 +44,10 @@ export default {
|
||||||
},
|
},
|
||||||
mixins: [configMixin],
|
mixins: [configMixin],
|
||||||
props: {
|
props: {
|
||||||
unreadMessages: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
conversationSize: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
availableAgents: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
hasFetched: {
|
hasFetched: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
conversationAttributes: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
unreadMessageCount: {
|
unreadMessageCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
|
@ -73,6 +58,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
unreadMessages: 'conversation/getUnreadTextMessages',
|
||||||
|
}),
|
||||||
showCloseButton() {
|
showCloseButton() {
|
||||||
return this.unreadMessageCount && this.hideMessageBubble;
|
return this.unreadMessageCount && this.hideMessageBubble;
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,18 +2,20 @@
|
||||||
#
|
#
|
||||||
# Table name: channel_web_widgets
|
# Table name: channel_web_widgets
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# feature_flags :integer default(3), not null
|
# feature_flags :integer default(3), not null
|
||||||
# hmac_token :string
|
# hmac_token :string
|
||||||
# reply_time :integer default("in_a_few_minutes")
|
# pre_chat_form_enabled :boolean default(FALSE)
|
||||||
# website_token :string
|
# pre_chat_form_options :jsonb
|
||||||
# website_url :string
|
# reply_time :integer default("in_a_few_minutes")
|
||||||
# welcome_tagline :string
|
# website_token :string
|
||||||
# welcome_title :string
|
# website_url :string
|
||||||
# widget_color :string default("#1f93ff")
|
# welcome_tagline :string
|
||||||
# created_at :datetime not null
|
# welcome_title :string
|
||||||
# updated_at :datetime not null
|
# widget_color :string default("#1f93ff")
|
||||||
# account_id :integer
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
9
app/views/api/v1/models/_widget_message.json.jbuilder
Normal file
9
app/views/api/v1/models/_widget_message.json.jbuilder
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
json.id resource.id
|
||||||
|
json.content resource.content
|
||||||
|
json.message_type resource.message_type_before_type_cast
|
||||||
|
json.content_type resource.content_type
|
||||||
|
json.content_attributes resource.content_attributes
|
||||||
|
json.created_at resource.created_at.to_i
|
||||||
|
json.conversation_id resource.conversation.display_id
|
||||||
|
json.attachments resource.attachments.map(&:push_event_data) if resource.attachments.present?
|
||||||
|
json.sender resource.sender.push_event_data if resource.sender
|
10
app/views/api/v1/widget/conversations/create.json.jbuilder
Normal file
10
app/views/api/v1/widget/conversations/create.json.jbuilder
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
json.id @conversation.display_id
|
||||||
|
json.inbox_id @conversation.inbox_id
|
||||||
|
json.contact_last_seen_at @conversation.contact_last_seen_at.to_i
|
||||||
|
json.status @conversation.status
|
||||||
|
json.messages do
|
||||||
|
json.array! @conversation.messages do |message|
|
||||||
|
json.partial! 'api/v1/models/widget_message', resource: message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
json.contact @conversation.contact
|
|
@ -19,7 +19,9 @@
|
||||||
widgetColor: '<%= @web_widget.widget_color %>',
|
widgetColor: '<%= @web_widget.widget_color %>',
|
||||||
enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
|
enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
|
||||||
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
|
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
|
||||||
replyTime: '<%= @web_widget.reply_time %>'
|
replyTime: '<%= @web_widget.reply_time %>',
|
||||||
|
preChatFormEnabled: <%= @web_widget.pre_chat_form_enabled %>,
|
||||||
|
preChatFormOptions: <%= @web_widget.pre_chat_form_options.to_json.html_safe %>
|
||||||
}
|
}
|
||||||
window.chatwootWidgetDefaults = {
|
window.chatwootWidgetDefaults = {
|
||||||
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,
|
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,
|
||||||
|
|
|
@ -139,7 +139,7 @@ Rails.application.routes.draw do
|
||||||
namespace :widget do
|
namespace :widget do
|
||||||
resources :events, only: [:create]
|
resources :events, only: [:create]
|
||||||
resources :messages, only: [:index, :create, :update]
|
resources :messages, only: [:index, :create, :update]
|
||||||
resources :conversations, only: [:index] do
|
resources :conversations, only: [:index, :create] do
|
||||||
collection do
|
collection do
|
||||||
post :update_last_seen
|
post :update_last_seen
|
||||||
post :toggle_typing
|
post :toggle_typing
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
class AddRequestForEmailOnChannelWebWidget < ActiveRecord::Migration[6.0]
|
||||||
|
def up
|
||||||
|
change_table :channel_web_widgets, bulk: true do |t|
|
||||||
|
t.column :pre_chat_form_enabled, :boolean, default: false
|
||||||
|
t.column :pre_chat_form_options, :jsonb, default: {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
change_table :channel_web_widgets, bulk: true do |t|
|
||||||
|
t.remove :pre_chat_form_enabled
|
||||||
|
t.remove :pre_chat_form_options
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_02_01_150037) do
|
ActiveRecord::Schema.define(version: 2021_02_12_154240) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -183,6 +183,8 @@ ActiveRecord::Schema.define(version: 2021_02_01_150037) do
|
||||||
t.integer "feature_flags", default: 3, null: false
|
t.integer "feature_flags", default: 3, null: false
|
||||||
t.integer "reply_time", default: 0
|
t.integer "reply_time", default: 0
|
||||||
t.string "hmac_token"
|
t.string "hmac_token"
|
||||||
|
t.boolean "pre_chat_form_enabled", default: false
|
||||||
|
t.jsonb "pre_chat_form_options", default: {}
|
||||||
t.index ["hmac_token"], name: "index_channel_web_widgets_on_hmac_token", unique: true
|
t.index ["hmac_token"], name: "index_channel_web_widgets_on_hmac_token", unique: true
|
||||||
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
|
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
|
||||||
end
|
end
|
||||||
|
@ -492,11 +494,9 @@ ActiveRecord::Schema.define(version: 2021_02_01_150037) do
|
||||||
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
|
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
|
||||||
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
|
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
|
||||||
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
|
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
|
||||||
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
|
|
||||||
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
|
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
|
||||||
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
|
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
|
||||||
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
|
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
|
||||||
t.index ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", id: :serial, force: :cascade do |t|
|
create_table "tags", id: :serial, force: :cascade do |t|
|
||||||
|
|
|
@ -27,6 +27,31 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/widget/conversations' do
|
||||||
|
it 'creates a conversation' do
|
||||||
|
post '/api/v1/widget/conversations',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: {
|
||||||
|
website_token: web_widget.website_token,
|
||||||
|
contact: {
|
||||||
|
name: 'contact-name',
|
||||||
|
email: 'contact-email@chatwoot.com'
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
content: 'This is a test message'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(json_response['id']).not_to eq nil
|
||||||
|
expect(json_response['contact']['email']).to eq 'contact-email@chatwoot.com'
|
||||||
|
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/widget/conversations/toggle_typing' do
|
describe 'POST /api/v1/widget/conversations/toggle_typing' do
|
||||||
context 'with a conversation' do
|
context 'with a conversation' do
|
||||||
it 'dispatches the correct typing status' do
|
it 'dispatches the correct typing status' do
|
||||||
|
|
Loading…
Reference in a new issue