Chore: Web widget Inbox Tech Debts (#738)

* Chore: Webwidget Inbox Tech Debts

* Additional customization options creating Web Widget
* Changes to edit Page for Web Widget
* Remove the WebWidget API end points
* Minor chores

Address: #680, #502

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2020-04-19 23:40:28 +05:30
committed by GitHub
parent c0ce70e87b
commit 941fbb0d72
46 changed files with 564 additions and 503 deletions

View File

@@ -252,7 +252,7 @@ linters:
enabled: false enabled: false
UnnecessaryParentReference: UnnecessaryParentReference:
enabled: true enabled: false
UrlFormat: UrlFormat:
enabled: true enabled: true

View File

@@ -4,16 +4,18 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
def register_facebook_page def register_facebook_page
user_access_token = params[:user_access_token] user_access_token = params[:user_access_token]
page_access_token = params[:page_access_token] page_access_token = params[:page_access_token]
page_name = params[:page_name]
page_id = params[:page_id] page_id = params[:page_id]
inbox_name = params[:inbox_name] inbox_name = params[:inbox_name]
facebook_channel = current_account.facebook_pages.create!( ActiveRecord::Base.transaction do
name: page_name, page_id: page_id, user_access_token: user_access_token, facebook_channel = current_account.facebook_pages.create!(
page_access_token: page_access_token page_id: page_id, user_access_token: user_access_token,
) page_access_token: page_access_token
set_avatar(facebook_channel, page_id) )
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel) @facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
render json: inbox set_avatar(@facebook_inbox, page_id)
rescue StandardError => e
Rails.logger e
end
end end
def facebook_pages def facebook_pages
@@ -72,13 +74,13 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
end end
end end
def set_avatar(facebook_channel, page_id) def set_avatar(facebook_inbox, page_id)
uri = get_avatar_url(page_id) uri = get_avatar_url(page_id)
return unless uri return unless uri
avatar_resource = LocalResource.new(uri) avatar_resource = LocalResource.new(uri)
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
end end
def get_avatar_url(page_id) def get_avatar_url(page_id)

View File

@@ -1,14 +1,24 @@
class Api::V1::Accounts::InboxesController < Api::BaseController class Api::V1::Accounts::InboxesController < Api::BaseController
before_action :check_authorization before_action :check_authorization
before_action :fetch_inbox, except: [:index] before_action :fetch_inbox, except: [:index, :create]
before_action :fetch_agent_bot, only: [:set_agent_bot] before_action :fetch_agent_bot, only: [:set_agent_bot]
def index def index
@inboxes = policy_scope(current_account.inboxes) @inboxes = policy_scope(current_account.inboxes)
end end
def create
ActiveRecord::Base.transaction do
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
@inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel)
@inbox.avatar.attach(permitted_params[:avatar])
@inbox.save!
end
end
def update def update
@inbox.update(inbox_update_params) @inbox.update(inbox_update_params.except(:channel))
@inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
end end
def set_agent_bot def set_agent_bot
@@ -37,11 +47,20 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end end
def web_widgets
current_account.web_widgets
end
def check_authorization def check_authorization
authorize(Inbox) authorize(Inbox)
end end
def permitted_params
params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message])
end
def inbox_update_params def inbox_update_params
params.require(:inbox).permit(:enable_auto_assignment, :avatar) params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title,
:welcome_tagline, :agent_away_message])
end end
end end

View File

@@ -1,48 +0,0 @@
class Api::V1::Accounts::Widget::InboxesController < Api::BaseController
before_action :authorize_request
before_action :set_web_widget_channel, only: [:update]
before_action :set_inbox, only: [:update]
def create
ActiveRecord::Base.transaction do
channel = web_widgets.create!(
website_name: permitted_params[:website][:website_name],
website_url: permitted_params[:website][:website_url],
widget_color: permitted_params[:website][:widget_color]
)
@inbox = inboxes.create!(name: permitted_params[:website][:website_name], channel: channel)
end
end
def update
@channel.update!(
widget_color: permitted_params[:website][:widget_color]
)
end
private
def authorize_request
authorize ::Inbox
end
def inboxes
current_account.inboxes
end
def web_widgets
current_account.web_widgets
end
def set_web_widget_channel
@channel = web_widgets.find_by(id: permitted_params[:id])
end
def set_inbox
@inbox = @channel.inbox
end
def permitted_params
params.permit(:id, website: [:website_name, :website_url, :widget_color])
end
end

View File

@@ -6,7 +6,7 @@ class Twitter::AuthorizationsController < Twitter::BaseController
::Redis::Alfred.setex(oauth_token, account.id) ::Redis::Alfred.setex(oauth_token, account.id)
redirect_to oauth_authorize_endpoint(oauth_token) redirect_to oauth_authorize_endpoint(oauth_token)
else else
redirect_to app_new_twitter_inbox_url redirect_to app_new_twitter_inbox_url(account_id: account.id)
end end
end end

View File

@@ -39,8 +39,7 @@ class Twitter::CallbacksController < Twitter::BaseController
twitter_profile = account.twitter_profiles.create( twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'], twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'], twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id'], profile_id: parsed_body['user_id']
name: parsed_body['screen_name']
) )
account.inboxes.create( account.inboxes.create(
name: parsed_body['screen_name'], name: parsed_body['screen_name'],

View File

@@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
class WebChannel extends ApiClient { class WebChannel extends ApiClient {
constructor() { constructor() {
super('widget/inboxes', { accountScoped: true }); super('inboxes', { accountScoped: true });
} }
} }

View File

@@ -31,42 +31,39 @@
.wizard-box { .wizard-box {
.item { .item {
@include padding($space-normal $space-normal $space-normal $space-medium); @include padding($space-normal $space-normal $space-normal $space-medium);
position: relative;
@include background-light; @include background-light;
cursor: pointer;
&:before, cursor: pointer;
&:after { position: relative;
content: '';
position: absolute; &::before,
width: 2px; &::after {
height: 100%;
background: $color-border; background: $color-border;
content: '';
height: 100%;
position: absolute;
top: $space-normal; top: $space-normal;
width: 2px;
} }
&:before { &::before {
top: $zero;
height: $space-normal; height: $space-normal;
top: $zero;
} }
&:first-child { &:first-child {
&:before { &::before {
height: 0; height: 0;
} }
} }
&:last-child { &:last-child {
&:after { &::after {
height: $zero; height: $zero;
} }
} }
&.active { &.active {
// left: 1px;
// @include background-white;
// @include border-light;
// border-right: 0;
h3 { h3 {
color: $color-woot; color: $color-woot;
} }
@@ -78,7 +75,7 @@
&.over { &.over {
&:after { &::after {
background: $color-woot; background: $color-woot;
} }
@@ -86,18 +83,18 @@
background: $color-woot; background: $color-woot;
} }
&+.item { & + .item {
&:before { &::before {
background: $color-woot; background: $color-woot;
} }
} }
} }
h3 { h3 {
font-size: $font-size-default;
padding-left: $space-medium;
line-height: 1;
color: $color-body; color: $color-body;
font-size: $font-size-default;
line-height: 1;
padding-left: $space-medium;
.completed { .completed {
color: $success-color; color: $success-color;
@@ -105,25 +102,25 @@
} }
p { p {
font-size: $font-size-small;
color: $color-light-gray; color: $color-light-gray;
padding-left: $space-medium; font-size: $font-size-small;
margin: 0; margin: 0;
padding-left: $space-medium;
} }
.step { .step {
position: absolute;
left: $space-normal;
top: $space-normal;
font-size: $font-size-small;
font-weight: $font-weight-medium;
background: $color-border; background: $color-border;
border-radius: 20px; border-radius: 20px;
width: $space-normal; color: $color-white;
font-size: $font-size-small;
font-weight: $font-weight-medium;
height: $space-normal; height: $space-normal;
text-align: center; left: $space-normal;
line-height: $space-normal; line-height: $space-normal;
color: #fff; position: absolute;
text-align: center;
top: $space-normal;
width: $space-normal;
z-index: 999; z-index: 999;
i { i {
@@ -141,10 +138,6 @@
} }
.inoboxes-list { .inoboxes-list {
// @include margin(auto);
// @include background-white;
// @include border-light;
// width: 50%;
.inbox-item { .inbox-item {
@include margin($space-normal); @include margin($space-normal);
@@ -152,16 +145,18 @@
@include flex-shrink; @include flex-shrink;
@include padding($space-normal $space-normal); @include padding($space-normal $space-normal);
@include border-light-bottom(); @include border-light-bottom();
flex-direction: column;
background: $color-white; background: $color-white;
cursor: pointer; cursor: pointer;
width: 20%; flex-direction: column;
float: left; float: left;
min-height: 10rem; min-height: 10rem;
width: 20%;
&:last-child { &:last-child {
margin-bottom: $zero;
@include border-nil; @include border-nil;
margin-bottom: $zero;
} }
&:hover { &:hover {
@@ -174,8 +169,8 @@
.switch { .switch {
align-self: center; align-self: center;
margin-right: $space-normal;
margin-bottom: $zero; margin-bottom: $zero;
margin-right: $space-normal;
} }
.item--details { .item--details {
@@ -187,15 +182,15 @@
} }
.item--sub { .item--sub {
margin-bottom: 0;
font-size: $font-size-small; font-size: $font-size-small;
margin-bottom: 0;
} }
} }
.arrow { .arrow {
align-self: center; align-self: center;
font-size: $font-size-small;
color: $medium-gray; color: $medium-gray;
font-size: $font-size-small;
opacity: .7; opacity: .7;
transform: translateX(0); transform: translateX(0);
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s; transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
@@ -204,18 +199,19 @@
} }
.settings--content { .settings--content {
@include margin($space-small $space-medium); @include margin($space-small $space-larger);
.title { .title {
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
} }
.code { .code {
@include padding($space-one);
background: $color-background;
max-height: $space-mega; max-height: $space-mega;
overflow: auto; overflow: auto;
white-space: nowrap; white-space: nowrap;
@include padding($space-one);
background: $color-background;
code { code {
background: transparent; background: transparent;
@@ -225,8 +221,8 @@
} }
.login-init { .login-init {
text-align: center;
padding-top: 30%; padding-top: 30%;
text-align: center;
p { p {
@include padding($space-medium); @include padding($space-medium);

View File

@@ -1,69 +0,0 @@
<template>
<div class="row settings--form--header">
<div class="medium-8">
<p class="title">
{{ title }}
</p>
<p class="sub-head">
{{ subTitle }}
</p>
</div>
<div v-if="buttonText" class="medium-4 text-right">
<woot-submit-button
class="default"
:button-text="buttonText"
:loading="isUpdating"
@click="onClick()"
/>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
subTitle: {
type: String,
required: true,
},
buttonText: {
type: String,
default: '',
},
isUpdating: {
type: Boolean,
default: false,
},
},
methods: {
onClick() {
this.$emit('update');
},
},
};
</script>
<style lang="scss">
@import '~dashboard/assets/scss/variables';
.settings--form--header {
align-items: center;
border-bottom: 1px solid $color-border;
display: flex;
margin-bottom: $space-normal;
padding: $space-normal 0;
.button {
margin-bottom: 0;
}
.title {
margin-bottom: 0;
font-size: $font-size-default;
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="row settings--section">
<div class="medium-4">
<p class="sub-block-title">
{{ title }}
</p>
<p class="sub-head">
{{ subTitle }}
</p>
</div>
<div class="medium-6">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
subTitle: {
type: String,
required: true,
},
},
};
</script>
<style lang="scss">
@import '~dashboard/assets/scss/variables';
.settings--section {
border-bottom: 1px solid $color-border;
display: flex;
padding: $space-normal 0;
.sub-block-title {
color: $color-woot;
font-weight: $font-weight-medium;
margin-bottom: 0;
}
}
</style>

View File

@@ -30,6 +30,18 @@
"LABEL": "Website Domain", "LABEL": "Website Domain",
"PLACEHOLDER": "Enter your website domain (eg: acme.com)" "PLACEHOLDER": "Enter your website domain (eg: acme.com)"
}, },
"CHANNEL_WELCOME_TITLE": {
"LABEL": "Welcome Heading",
"PLACEHOLDER": "Hi there !"
},
"CHANNEL_WELCOME_TAGLINE": {
"LABEL": "Welcome Tagline",
"PLACEHOLDER": "We make it simple to connect with us. Ask us anything, or share your feedback."
},
"CHANNEL_AGENT_AWAY_MESSAGE": {
"LABEL": "Agents Away Message",
"PLACEHOLDER": "Acme Inc typically replies in a few hours."
},
"WIDGET_COLOR": { "WIDGET_COLOR": {
"LABEL": "Widget Color", "LABEL": "Widget Color",
"PLACEHOLDER": "Update the widget color used in widget" "PLACEHOLDER": "Update the widget color used in widget"
@@ -102,7 +114,7 @@
"VIEW": "View", "VIEW": "View",
"EDIT": { "EDIT": {
"API": { "API": {
"SUCCESS_MESSAGE": "Widget color updated successfully", "SUCCESS_MESSAGE": "Inbox settings updated successfully",
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Auto assignment updated successfully", "AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Auto assignment updated successfully",
"ERROR_MESSAGE": "Could not update widget color. Please try again later." "ERROR_MESSAGE": "Could not update widget color. Please try again later."
}, },
@@ -132,7 +144,9 @@
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
"UPDATE": "Update", "UPDATE": "Update",
"AUTO_ASSIGNMENT": "Enable auto assignment", "AUTO_ASSIGNMENT": "Enable auto assignment",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of available agents on new conversations" "INBOX_UPDATE_TITLE": "Inbox Settings",
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox."
} }
} }
} }

View File

@@ -6,7 +6,7 @@
"ACCOUNT_NAME": { "ACCOUNT_NAME": {
"LABEL": "Account Name", "LABEL": "Account Name",
"PLACEHOLDER": "Wayne Enterprises", "PLACEHOLDER": "Wayne Enterprises",
"ERROR": "Account Name is too small" "ERROR": "Account Name is too short"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "Email", "LABEL": "Email",

View File

@@ -4,79 +4,177 @@
:header-image="inbox.avatarUrl" :header-image="inbox.avatarUrl"
:header-title="inboxName" :header-title="inboxName"
/> />
<div class="settings--content">
<settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')"
>
<div v-if="inbox.channel_type === 'Channel::WebWidget'">
<div class="medium-12 columns">
<label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL') }}
<input
v-model.trim="inboxName"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL') }}
<input
v-model.trim="channelWebsiteUrl"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.LABEL')
}}
<input
v-model.trim="channelWelcomeTitle"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL'
)
}}
<input
v-model.trim="channelWelcomeTagline"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AGENT_AWAY_MESSAGE.LABEL'
)
}}
<input
v-model.trim="channelAgentAwayMessage"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AGENT_AWAY_MESSAGE.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL') }}
<compact
v-model="inbox.widget_color"
class="widget-color--selector"
/>
</label>
</div>
</div>
<div>
<label>
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT') }}
<select v-model="autoAssignment">
<option value="true">
{{ $t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.ENABLED') }}
</option>
<option value="false">
{{ $t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.DISABLED') }}
</option>
</select>
<p class="help-text">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
</p>
</label>
</div>
<woot-submit-button
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:loading="uiFlags.isUpdatingInbox"
@click="updateInbox"
>
</woot-submit-button>
</settings-section>
</div>
<!-- update agents in inbox -->
<div class="settings--content">
<settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
>
<multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
placeholder="Pick some"
@select="$v.selectedAgents.$touch"
/>
<woot-submit-button
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:loading="isAgentListUpdating"
@click="updateAgents"
>
</woot-submit-button>
</settings-section>
</div>
<div <div
v-if="inbox.channel_type === 'Channel::FacebookPage'" v-if="inbox.channel_type === 'Channel::FacebookPage'"
class="settings--content" class="settings--content"
> >
<settings-form-header <settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')" :title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')" :sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
> >
</settings-form-header> <woot-code :script="messengerScript"></woot-code>
<woot-code :script="messengerScript"></woot-code> </settings-section>
</div> </div>
<div v-else-if="inbox.channel_type === 'Channel::WebWidget'"> <div v-else-if="inbox.channel_type === 'Channel::WebWidget'">
<div class="settings--content"> <div class="settings--content">
<settings-form-header <settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')" :title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')" :sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
> >
</settings-form-header> <woot-code :script="inbox.web_widget_script"></woot-code>
<woot-code :script="inbox.web_widget_script"></woot-code> </settings-section>
</div> </div>
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL')"
:sub-title="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.PLACEHOLDER')
"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="uiFlags.isUpdating"
@update="updateWidgetColor"
>
</settings-form-header>
<Compact v-model="inbox.widget_color" class="widget-color--selector" />
</div>
</div>
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="isAgentListUpdating"
@update="updateAgents"
>
</settings-form-header>
<multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
placeholder="Pick some"
@select="$v.selectedAgents.$touch"
/>
</div>
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT')"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="uiFlags.isUpdatingAutoAssignment"
@update="updateAutoAssignment"
>
</settings-form-header>
<select v-model="autoAssignment">
<option value="true">
{{ $t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.ENABLED') }}
</option>
<option value="false">
{{ $t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.DISABLED') }}
</option>
</select>
</div> </div>
</div> </div>
</template> </template>
@@ -88,12 +186,12 @@ import { mapGetters } from 'vuex';
import { createMessengerScript } from 'dashboard/helper/scriptGenerator'; import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
import { Compact } from 'vue-color'; import { Compact } from 'vue-color';
import configMixin from 'shared/mixins/configMixin'; import configMixin from 'shared/mixins/configMixin';
import SettingsFormHeader from '../../../../components/SettingsFormHeader.vue'; import SettingsSection from '../../../../components/SettingsSection';
export default { export default {
components: { components: {
Compact, Compact,
SettingsFormHeader, SettingsSection,
}, },
mixins: [configMixin], mixins: [configMixin],
data() { data() {
@@ -102,6 +200,10 @@ export default {
autoAssignment: false, autoAssignment: false,
isUpdating: false, isUpdating: false,
isAgentListUpdating: false, isAgentListUpdating: false,
channelWebsiteUrl: '',
channelWelcomeTitle: '',
channelWelcomeTagline: '',
channelAgentAwayMessage: '',
}; };
}, },
computed: { computed: {
@@ -145,6 +247,10 @@ export default {
this.$store.dispatch('inboxes/get').then(() => { this.$store.dispatch('inboxes/get').then(() => {
this.fetchAttachedAgents(); this.fetchAttachedAgents();
this.autoAssignment = this.inbox.enable_auto_assignment; this.autoAssignment = this.inbox.enable_auto_assignment;
this.channelWebsiteUrl = this.inbox.website_url;
this.channelWelcomeTitle = this.inbox.welcome_title;
this.channelWelcomeTagline = this.inbox.welcome_tagline;
this.channelAgentAwayMessage = this.inbox.agent_away_message;
}); });
}, },
async fetchAttachedAgents() { async fetchAttachedAgents() {
@@ -181,34 +287,23 @@ export default {
} }
this.isAgentListUpdating = false; this.isAgentListUpdating = false;
}, },
async updateWidgetColor() { async updateInbox() {
try { try {
await this.$store.dispatch('inboxes/updateWebsiteChannel', { await this.$store.dispatch('inboxes/updateInbox', {
id: this.inbox.channel_id,
website: {
widget_color: this.getWidgetColor(this.inbox.widget_color),
},
});
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
}
},
async updateAutoAssignment() {
try {
await this.$store.dispatch('inboxes/updateAutoAssignment', {
id: this.currentInboxId, id: this.currentInboxId,
inbox: { name: this.inboxName,
enable_auto_assignment: this.autoAssignment, enable_auto_assignment: this.autoAssignment,
channel: {
widget_color: this.getWidgetColor(this.inbox.widget_color),
website_url: this.channelWebsiteUrl,
welcome_title: this.channelWelcomeTitle,
welcome_tagline: this.channelWelcomeTagline,
agent_away_message: this.channelAgentAwayMessage,
}, },
}); });
this.showAlert( this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
this.$t('INBOX_MGMT.EDIT.API.AUTO_ASSIGNMENT_SUCCESS_MESSAGE')
);
} catch (error) { } catch (error) {
this.showAlert( this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
this.$t('INBOX_MGMT.EDIT.API.AUTO_ASSIGNMENT_SUCCESS_MESSAGE')
);
} }
}, },
getWidgetColor() { getWidgetColor() {
@@ -226,3 +321,26 @@ export default {
}, },
}; };
</script> </script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/mixins';
.settings {
background: $color-white;
.settings--content {
&:last-child {
.settings--section {
border-bottom: 0;
}
}
}
.page-top-bar {
@include background-light;
@include border-normal-bottom;
padding: $space-normal $space-larger;
}
}
</style>

View File

@@ -221,9 +221,8 @@ export default {
return { return {
user_access_token: this.user_access_token, user_access_token: this.user_access_token,
page_access_token: this.selectedPage.access_token, page_access_token: this.selectedPage.access_token,
page_name: this.selectedPage.name,
page_id: this.selectedPage.id, page_id: this.selectedPage.id,
inbox_name: this.pageName, inbox_name: this.selectedPage.name,
}; };
}, },

View File

@@ -18,7 +18,7 @@
<label> <label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL') }} {{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL') }}
<input <input
v-model.trim="websiteName" v-model.trim="inboxName"
type="text" type="text"
:placeholder=" :placeholder="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.PLACEHOLDER') $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
@@ -30,7 +30,7 @@
<label> <label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL') }} {{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL') }}
<input <input
v-model.trim="websiteUrl" v-model.trim="channelWebsiteUrl"
type="text" type="text"
:placeholder=" :placeholder="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.PLACEHOLDER') $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.PLACEHOLDER')
@@ -38,11 +38,62 @@
/> />
</label> </label>
</div> </div>
<div class="medium-12 columns">
<label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.LABEL') }}
<input
v-model.trim="channelWelcomeTitle"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TITLE.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.LABEL')
}}
<input
v-model.trim="channelWelcomeTagline"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_WELCOME_TAGLINE.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns">
<label>
{{
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AGENT_AWAY_MESSAGE.LABEL'
)
}}
<input
v-model.trim="channelAgentAwayMessage"
type="text"
:placeholder="
$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_AGENT_AWAY_MESSAGE.PLACEHOLDER'
)
"
/>
</label>
</div>
<div class="medium-12 columns"> <div class="medium-12 columns">
<label> <label>
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL') }} {{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL') }}
<compact v-model="widgetColor" class="widget-color--selector" /> <compact
v-model="channelWidgetColor"
class="widget-color--selector"
/>
</label> </label>
</div> </div>
@@ -50,7 +101,7 @@
<div class="medium-12 columns"> <div class="medium-12 columns">
<woot-submit-button <woot-submit-button
:loading="uiFlags.isCreating" :loading="uiFlags.isCreating"
:disabled="!websiteUrl || !websiteName" :disabled="!channelWebsiteUrl || !inboxName"
:button-text="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.SUBMIT_BUTTON')" :button-text="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.SUBMIT_BUTTON')"
/> />
</div> </div>
@@ -72,9 +123,12 @@ export default {
}, },
data() { data() {
return { return {
websiteName: '', inboxName: '',
websiteUrl: '', channelWebsiteUrl: '',
widgetColor: { hex: '#009CE0' }, channelWidgetColor: { hex: '#009CE0' },
channelWelcomeTitle: '',
channelWelcomeTagline: '',
channelAgentAwayMessage: '',
}; };
}, },
computed: { computed: {
@@ -87,10 +141,14 @@ export default {
const website = await this.$store.dispatch( const website = await this.$store.dispatch(
'inboxes/createWebsiteChannel', 'inboxes/createWebsiteChannel',
{ {
website: { name: this.inboxName,
website_name: this.websiteName, channel: {
website_url: this.websiteUrl, type: 'web_widget',
widget_color: this.widgetColor.hex, website_url: this.channelWebsiteUrl,
widget_color: this.channelWidgetColor.hex,
welcome_title: this.channelWelcomeTitle,
welcome_tagline: this.channelWelcomeTagline,
agent_away_message: this.channelAgentAwayMessage,
}, },
} }
); );

View File

@@ -79,18 +79,7 @@ export const actions = {
throw new Error(error); throw new Error(error);
} }
}, },
updateWebsiteChannel: async ({ commit }, { id, ...inboxParams }) => { updateInbox: async ({ commit }, { id, ...inboxParams }) => {
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: true });
try {
const response = await WebChannel.update(id, inboxParams);
commit(types.default.EDIT_INBOXES, response.data);
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: false });
throw new Error(error);
}
},
updateAutoAssignment: async ({ commit }, { id, ...inboxParams }) => {
commit(types.default.SET_INBOXES_UI_FLAG, { commit(types.default.SET_INBOXES_UI_FLAG, {
isUpdatingAutoAssignment: true, isUpdatingAutoAssignment: true,
}); });

View File

@@ -70,35 +70,13 @@ describe('#actions', () => {
}); });
}); });
describe('#updateWebsiteChannel', () => { describe('#updateInbox', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: inboxList[0] });
await actions.updateWebsiteChannel({ commit }, inboxList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: true }],
[types.default.EDIT_INBOXES, inboxList[0]],
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.updateWebsiteChannel({ commit }, inboxList[0])
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: true }],
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: false }],
]);
});
});
describe('#updateAutoAssignment', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
const updatedInbox = inboxList[0]; const updatedInbox = inboxList[0];
updatedInbox.enable_auto_assignment = false; updatedInbox.enable_auto_assignment = false;
axios.patch.mockResolvedValue({ data: updatedInbox }); axios.patch.mockResolvedValue({ data: updatedInbox });
await actions.updateAutoAssignment( await actions.updateInbox(
{ commit }, { commit },
{ id: updatedInbox.id, inbox: { enable_auto_assignment: false } } { id: updatedInbox.id, inbox: { enable_auto_assignment: false } }
); );
@@ -114,7 +92,7 @@ describe('#actions', () => {
it('sends correct actions if API is error', async () => { it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' }); axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect( await expect(
actions.updateAutoAssignment( actions.updateInbox(
{ commit }, { commit },
{ id: inboxList[0].id, inbox: { enable_auto_assignment: false } } { id: inboxList[0].id, inbox: { enable_auto_assignment: false } }
) )

View File

@@ -93,7 +93,7 @@ export const IFrameHelper = {
} }
}, },
}, },
onLoad: ({ widget_color: widgetColor }) => { onLoad: ({ widgetColor }) => {
const iframe = IFrameHelper.getAppFrame(); const iframe = IFrameHelper.getAppFrame();
iframe.style.visibility = ''; iframe.style.visibility = '';
iframe.setAttribute('id', `chatwoot_live_chat_widget`); iframe.setAttribute('id', `chatwoot_live_chat_widget`);

View File

@@ -17,7 +17,7 @@ export default {
}; };
}, },
mounted() { mounted() {
const { website_token: websiteToken = '' } = window.chatwootWebChannel; const { websiteToken } = window.chatwootWebChannel;
if (IFrameHelper.isIFrame()) { if (IFrameHelper.isIFrame()) {
IFrameHelper.sendMessage({ IFrameHelper.sendMessage({
event: 'loaded', event: 'loaded',

View File

@@ -20,11 +20,6 @@ import { IFrameHelper } from 'widget/helpers/utils';
export default { export default {
name: 'ChatHeaderExpanded', name: 'ChatHeaderExpanded',
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
}),
},
props: { props: {
introHeading: { introHeading: {
type: String, type: String,
@@ -36,6 +31,11 @@ export default {
'We make it simple to connect with us. Ask us anything, or share your feedback.', 'We make it simple to connect with us. Ask us anything, or share your feedback.',
}, },
}, },
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
}),
},
methods: { methods: {
closeWindow() { closeWindow() {
if (IFrameHelper.isIFrame()) { if (IFrameHelper.isIFrame()) {

View File

@@ -16,7 +16,7 @@ const actions = {
const mutations = { const mutations = {
[SET_WIDGET_COLOR]($state, data) { [SET_WIDGET_COLOR]($state, data) {
$state.widgetColor = data.widget_color; $state.widgetColor = data.widgetColor;
}, },
}; };

View File

@@ -1,8 +1,12 @@
<template> <template>
<div class="home"> <div class="home">
<div class="header-wrap"> <div class="header-wrap">
<ChatHeaderExpanded v-if="isHeaderExpanded" /> <ChatHeaderExpanded
<ChatHeader v-else :title="getHeaderName" /> v-if="isHeaderExpanded"
:intro-heading="channelConfig.welcomeTitle"
:intro-body="channelConfig.welcomeTagline"
/>
<ChatHeader v-else :title="channelConfig.websiteName" />
</div> </div>
<AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" /> <AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" />
<ConversationWrap :grouped-messages="groupedMessages" /> <ConversationWrap :grouped-messages="groupedMessages" />
@@ -45,8 +49,8 @@ export default {
isHeaderExpanded() { isHeaderExpanded() {
return this.conversationSize === 0; return this.conversationSize === 0;
}, },
getHeaderName() { channelConfig() {
return window.chatwootWebChannel.website_name; return window.chatwootWebChannel;
}, },
showAvailableAgents() { showAvailableAgents() {
return this.availableAgents.length > 0 && this.conversationSize < 1; return this.availableAgents.length > 0 && this.conversationSize < 1;

View File

@@ -3,7 +3,6 @@
# Table name: channel_facebook_pages # Table name: channel_facebook_pages
# #
# id :integer not null, primary key # id :integer not null, primary key
# name :string not null
# page_access_token :string not null # page_access_token :string not null
# user_access_token :string not null # user_access_token :string not null
# created_at :datetime not null # created_at :datetime not null
@@ -18,23 +17,19 @@
# #
class Channel::FacebookPage < ApplicationRecord class Channel::FacebookPage < ApplicationRecord
# FIXME: this should be removed post 1.4 release. we moved avatars to inbox
include Avatarable include Avatarable
self.table_name = 'channel_facebook_pages' self.table_name = 'channel_facebook_pages'
validates :account_id, presence: true validates :account_id, presence: true
validates :page_id, uniqueness: { scope: :account_id } validates :page_id, uniqueness: { scope: :account_id }
has_one_attached :avatar
belongs_to :account belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy has_one :inbox, as: :channel, dependent: :destroy
before_destroy :unsubscribe before_destroy :unsubscribe
def name
'Facebook'
end
private private
def unsubscribe def unsubscribe

View File

@@ -3,7 +3,6 @@
# Table name: channel_twitter_profiles # Table name: channel_twitter_profiles
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# name :string
# twitter_access_token :string not null # twitter_access_token :string not null
# twitter_access_token_secret :string not null # twitter_access_token_secret :string not null
# created_at :datetime not null # created_at :datetime not null
@@ -21,17 +20,12 @@ class Channel::TwitterProfile < ApplicationRecord
validates :account_id, presence: true validates :account_id, presence: true
validates :profile_id, uniqueness: { scope: :account_id } validates :profile_id, uniqueness: { scope: :account_id }
has_one_attached :avatar
belongs_to :account belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy has_one :inbox, as: :channel, dependent: :destroy
before_destroy :unsubscribe before_destroy :unsubscribe
def name
'Twitter'
end
def create_contact_inbox(profile_id, name, additional_attributes) def create_contact_inbox(profile_id, name, additional_attributes)
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name) contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name)

View File

@@ -2,14 +2,16 @@
# #
# Table name: channel_web_widgets # Table name: channel_web_widgets
# #
# id :integer not null, primary key # id :integer not null, primary key
# website_name :string # agent_away_message :string
# website_token :string # website_token :string
# website_url :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
# #
@@ -19,7 +21,6 @@
class Channel::WebWidget < ApplicationRecord class Channel::WebWidget < ApplicationRecord
self.table_name = 'channel_web_widgets' self.table_name = 'channel_web_widgets'
validates :website_name, presence: true
validates :website_url, presence: true validates :website_url, presence: true
validates :widget_color, presence: true validates :widget_color, presence: true
@@ -27,10 +28,6 @@ class Channel::WebWidget < ApplicationRecord
has_one :inbox, as: :channel, dependent: :destroy has_one :inbox, as: :channel, dependent: :destroy
has_secure_token :website_token has_secure_token :website_token
def name
'Website'
end
def web_widget_script def web_widget_script
"<script> "<script>
(function(d,t) { (function(d,t) {

View File

@@ -25,10 +25,10 @@ class NotificationSetting < ApplicationRecord
flag_query_mode: :bit_operator flag_query_mode: :bit_operator
}.freeze }.freeze
EMAIL_NOTIFCATION_FLAGS = { EMAIL_NOTIFICATION_FLAGS = {
1 => :conversation_creation, 1 => :conversation_creation,
2 => :conversation_assignment 2 => :conversation_assignment
}.freeze }.freeze
has_flags EMAIL_NOTIFCATION_FLAGS.merge(column: 'email_flags').merge(DEFAULT_QUERY_SETTING) has_flags EMAIL_NOTIFICATION_FLAGS.merge(column: 'email_flags').merge(DEFAULT_QUERY_SETTING)
end end

View File

@@ -0,0 +1,7 @@
json.id @facebook_inbox.id
json.channel_id @facebook_inbox.channel_id
json.name @facebook_inbox.name
json.channel_type @facebook_inbox.channel_type
json.avatar_url @facebook_inbox.try(:avatar_url)
json.page_id @facebook_inbox.channel.try(:page_id)
json.enable_auto_assignment @facebook_inbox.enable_auto_assignment

View File

@@ -4,4 +4,8 @@ json.name @inbox.name
json.channel_type @inbox.channel_type json.channel_type @inbox.channel_type
json.website_token @inbox.channel.try(:website_token) json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color) json.widget_color @inbox.channel.try(:widget_color)
json.website_url @inbox.channel.try(:website_url)
json.welcome_title @inbox.channel.try(:welcome_title)
json.welcome_tagline @inbox.channel.try(:welcome_tagline)
json.agent_away_message @inbox.channel.try(:agent_away_message)
json.web_widget_script @inbox.channel.try(:web_widget_script) json.web_widget_script @inbox.channel.try(:web_widget_script)

View File

@@ -4,10 +4,13 @@ json.payload do
json.channel_id inbox.channel_id json.channel_id inbox.channel_id
json.name inbox.name json.name inbox.name
json.channel_type inbox.channel_type json.channel_type inbox.channel_type
json.avatar_url inbox.channel.try(:avatar_url) json.avatar_url inbox.try(:avatar_url)
json.page_id inbox.channel.try(:page_id) json.page_id inbox.channel.try(:page_id)
json.widget_color inbox.channel.try(:widget_color) json.widget_color inbox.channel.try(:widget_color)
json.website_token inbox.channel.try(:website_token) json.website_url inbox.channel.try(:website_url)
json.welcome_title inbox.channel.try(:welcome_title)
json.welcome_tagline inbox.channel.try(:welcome_tagline)
json.agent_away_message inbox.channel.try(:agent_away_message)
json.enable_auto_assignment inbox.enable_auto_assignment json.enable_auto_assignment inbox.enable_auto_assignment
json.web_widget_script inbox.channel.try(:web_widget_script) json.web_widget_script inbox.channel.try(:web_widget_script)
json.phone_number inbox.channel.try(:phone_number) json.phone_number inbox.channel.try(:phone_number)

View File

@@ -0,0 +1,11 @@
json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color)
json.website_url @inbox.channel.try(:website_url)
json.welcome_title @inbox.channel.try(:welcome_title)
json.welcome_tagline @inbox.channel.try(:welcome_tagline)
json.agent_away_message @inbox.channel.try(:agent_away_message)
json.web_widget_script @inbox.channel.try(:web_widget_script)

View File

@@ -1,7 +0,0 @@
json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.website_token @inbox.channel.website_token
json.widget_color @inbox.channel.widget_color
json.web_widget_script @inbox.channel.try(:web_widget_script)

View File

@@ -6,9 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<script> <script>
window.chatwootWebChannel = { window.chatwootWebChannel = {
website_name: '<%= @web_widget.website_name %>', websiteName: '<%= @web_widget.inbox.name %>',
widget_color: '<%= @web_widget.widget_color %>', widgetColor: '<%= @web_widget.widget_color %>',
website_token: '<%= @web_widget.website_token %>' websiteToken: '<%= @web_widget.welcome_title %>',
welcomeTitle: '<%= @web_widget.welcome_title %>',
welcomeTagline: '<%= @web_widget.welcome_tagline %>',
} }
window.chatwootPubsubToken = '<%= @contact.pubsub_token %>' window.chatwootPubsubToken = '<%= @contact.pubsub_token %>'
window.authToken = '<%= @token %>' window.authToken = '<%= @token %>'

View File

@@ -66,7 +66,7 @@ Rails.application.routes.draw do
end end
end end
resources :inboxes, only: [:index, :destroy, :update] do resources :inboxes, only: [:index, :create, :update, :destroy] do
post :set_agent_bot, on: :member post :set_agent_bot, on: :member
end end
resources :inbox_members, only: [:create, :show], param: :inbox_id resources :inbox_members, only: [:create, :show], param: :inbox_id
@@ -97,9 +97,6 @@ Rails.application.routes.draw do
end end
resources :webhooks, except: [:show] resources :webhooks, except: [:show]
namespace :widget do
resources :inboxes, only: [:create, :update]
end
end end
# end of account scoped api routes # end of account scoped api routes

View File

@@ -0,0 +1,35 @@
class RemoveNameFromChannels < ActiveRecord::Migration[6.0]
def change
remove_column :channel_facebook_pages, :name, :string
remove_column :channel_twitter_profiles, :name, :string
migrate_web_widget_name_to_inbox
remove_column :channel_web_widgets, :website_name, :string # rubocop:disable Rails/BulkChangeTable
add_column :channel_web_widgets, :welcome_title, :string
add_column :channel_web_widgets, :welcome_tagline, :string
add_column :channel_web_widgets, :agent_away_message, :string
purge_orphan_facebook_pages
remove_avatars_from_channel_to_inbox
end
def purge_orphan_facebook_pages
Channel::FacebookPage.all.each do |facebook_page|
facebook_page.destroy! if facebook_page.inbox.nil?
end
end
def migrate_web_widget_name_to_inbox
Channel::WebWidget.all.each do |widget|
widget.inbox.name = widget.website_name
widget.save!
end
end
def remove_avatars_from_channel_to_inbox
Channel::FacebookPage.all.each do |facebook_page|
next unless facebook_page.avatar
facebook_page.avatar.purge
end
end
end

View File

@@ -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: 2020_04_04_135009) do ActiveRecord::Schema.define(version: 2020_04_17_093432) 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 "plpgsql" enable_extension "plpgsql"
@@ -104,7 +104,6 @@ ActiveRecord::Schema.define(version: 2020_04_04_135009) do
end end
create_table "channel_facebook_pages", id: :serial, force: :cascade do |t| create_table "channel_facebook_pages", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.string "page_id", null: false t.string "page_id", null: false
t.string "user_access_token", null: false t.string "user_access_token", null: false
t.string "page_access_token", null: false t.string "page_access_token", null: false
@@ -126,7 +125,6 @@ ActiveRecord::Schema.define(version: 2020_04_04_135009) do
end end
create_table "channel_twitter_profiles", force: :cascade do |t| create_table "channel_twitter_profiles", force: :cascade do |t|
t.string "name"
t.string "profile_id", null: false t.string "profile_id", null: false
t.string "twitter_access_token", null: false t.string "twitter_access_token", null: false
t.string "twitter_access_token_secret", null: false t.string "twitter_access_token_secret", null: false
@@ -137,13 +135,15 @@ ActiveRecord::Schema.define(version: 2020_04_04_135009) do
end end
create_table "channel_web_widgets", id: :serial, force: :cascade do |t| create_table "channel_web_widgets", id: :serial, force: :cascade do |t|
t.string "website_name"
t.string "website_url" t.string "website_url"
t.integer "account_id" t.integer "account_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "website_token" t.string "website_token"
t.string "widget_color", default: "#1f93ff" t.string "widget_color", default: "#1f93ff"
t.string "welcome_title"
t.string "welcome_tagline"
t.string "agent_away_message"
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

View File

@@ -10,7 +10,7 @@ AccountUser.create!(
role: :administrator role: :administrator
) )
web_widget = Channel::WebWidget.create!(account: account, website_name: 'Acme', website_url: 'https://acme.inc') web_widget = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc')
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
InboxMember.create!(user: user, inbox: inbox) InboxMember.create!(user: user, inbox: inbox)

View File

@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe 'Callbacks API', type: :request do RSpec.describe 'Callbacks API', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:valid_params) { attributes_for(:channel_facebook_page).merge(page_name: 'Test', inbox_name: 'Test Inbox') } let(:valid_params) { attributes_for(:channel_facebook_page).merge(inbox_name: 'Test Inbox') }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let!(:facebook_page) { create(:channel_facebook_page, inbox: inbox, account: account) } let!(:facebook_page) { create(:channel_facebook_page, inbox: inbox, account: account) }

View File

@@ -88,6 +88,44 @@ RSpec.describe 'Inboxes API', type: :request do
end end
end end
describe 'POST /api/v1/accounts/{account.id}/inboxes' do
let(:inbox) { create(:inbox, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/inboxes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:valid_params) { { name: 'test', channel: { type: 'web_widget', website_url: 'test.com' } } }
it 'creates inbox' do
post "/api/v1/accounts/#{account.id}/inboxes",
headers: admin.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('test.com')
end
it 'will not create inbox for agent' do
agent = create(:user, account: account, role: :agent)
post "/api/v1/accounts/#{account.id}/inboxes",
headers: agent.create_new_auth_token,
params: valid_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/inboxes/:id' do describe 'PATCH /api/v1/accounts/{account.id}/inboxes/:id' do
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
@@ -101,7 +139,7 @@ RSpec.describe 'Inboxes API', type: :request do
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) } let(:admin) { create(:user, account: account, role: :administrator) }
let(:valid_params) { { inbox: { enable_auto_assignment: false } } } let(:valid_params) { { enable_auto_assignment: false, channel: { website_url: 'test.com' } } }
it 'updates inbox' do it 'updates inbox' do
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
@@ -118,10 +156,11 @@ RSpec.describe 'Inboxes API', type: :request do
expect(inbox.avatar.attached?).to eq(false) expect(inbox.avatar.attached?).to eq(false)
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
params: { inbox: { avatar: file } }, params: valid_params.merge(avatar: file),
headers: admin.create_new_auth_token headers: admin.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(response.body).to include('test.com')
inbox.reload inbox.reload
expect(inbox.avatar.attached?).to eq(true) expect(inbox.avatar.attached?).to eq(true)
end end

View File

@@ -1,79 +0,0 @@
require 'rails_helper'
RSpec.describe '/api/v1/accounts/{account.id}/widget/inboxes', type: :request do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'POST /api/v1/accounts/{account.id}/widget/inboxes' do
let(:params) { { website: { website_name: 'test', website_url: 'test.com', widget_color: '#eaeaea' } } }
context 'when unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/widget/inboxes", params: params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is logged in' do
context 'with user as administrator' do
it 'creates inbox and returns website_token' do
post "/api/v1/accounts/#{account.id}/widget/inboxes", params: params, headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['name']).to eq('test')
expect(json_response['website_token']).not_to be_empty
end
end
context 'with user as agent' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/widget/inboxes",
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/widget/inboxes/:id' do
let(:update_params) { { website: { widget_color: '#eaeaea' } } }
context 'when unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/widget/inboxes/#{inbox.channel_id}", params: update_params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is logged in' do
context 'with user as administrator' do
it 'updates website channel' do
patch "/api/v1/accounts/#{account.id}/widget/inboxes/#{inbox.channel_id}",
params: update_params,
headers: admin.create_new_auth_token
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['widget_color']).to eq('#eaeaea')
end
end
context 'with user as agent' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/widget/inboxes/#{inbox.channel_id}",
params: update_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
end
end
end

View File

@@ -2,7 +2,6 @@
FactoryBot.define do FactoryBot.define do
factory :channel_widget, class: 'Channel::WebWidget' do factory :channel_widget, class: 'Channel::WebWidget' do
sequence(:website_name) { |n| "Example Website #{n}" }
sequence(:website_url) { |n| "https://example-#{n}.com" } sequence(:website_url) { |n| "https://example-#{n}.com" }
sequence(:widget_color, &:to_s) sequence(:widget_color, &:to_s)
account account

View File

@@ -2,7 +2,6 @@
FactoryBot.define do FactoryBot.define do
factory :channel_facebook_page, class: 'Channel::FacebookPage' do factory :channel_facebook_page, class: 'Channel::FacebookPage' do
name { Faker::Name.name }
page_access_token { SecureRandom.uuid } page_access_token { SecureRandom.uuid }
user_access_token { SecureRandom.uuid } user_access_token { SecureRandom.uuid }
page_id { SecureRandom.uuid } page_id { SecureRandom.uuid }

View File

@@ -2,7 +2,6 @@
FactoryBot.define do FactoryBot.define do
factory :channel_twitter_profile, class: 'Channel::TwitterProfile' do factory :channel_twitter_profile, class: 'Channel::TwitterProfile' do
name { Faker::Name.name }
twitter_access_token { SecureRandom.uuid } twitter_access_token { SecureRandom.uuid }
twitter_access_token_secret { SecureRandom.uuid } twitter_access_token_secret { SecureRandom.uuid }
profile_id { SecureRandom.uuid } profile_id { SecureRandom.uuid }

View File

@@ -1,11 +0,0 @@
# frozen_string_literal: true
FactoryBot.define do
factory :facebook_page, class: 'Channel::FacebookPage' do
sequence(:page_id) { |n| n }
sequence(:user_access_token) { |n| "random-token-#{n}" }
sequence(:name) { |n| "Facebook Page #{n}" }
sequence(:page_access_token) { |n| "page-access-token-#{n}" }
account
end
end

View File

@@ -11,7 +11,7 @@ describe Facebook::SendReplyService do
let!(:account) { create(:account) } let!(:account) { create(:account) }
let(:bot) { class_double('Bot').as_stubbed_const } let(:bot) { class_double('Bot').as_stubbed_const }
let!(:widget_inbox) { create(:inbox, account: account) } let!(:widget_inbox) { create(:inbox, account: account) }
let!(:facebook_channel) { create(:facebook_page, account: account) } let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) } let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
let!(:contact) { create(:contact, account: account) } let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: facebook_inbox) } let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: facebook_inbox) }

View File

@@ -1,28 +0,0 @@
patch:
tags:
- Widget
operationId: widgetInboxUpdate
summary: Update a website inbox
description: Update widget color of an inbox
parameters:
- name: data
in: body
required: true
schema:
type: object
properties:
website:
type: object
properties:
widget_color:
type: string
description: A Hex-color string used to customize the widget
responses:
200:
description: Success
schema:
$ref: '#/definitions/inbox'
404:
description: Inbox not found
403:
description: Access denied