feat: Disable attachments and emoji picker in the web widget (#1102)

Signed-off-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
Pranav Raj S 2020-08-05 17:46:17 +05:30 committed by GitHub
parent 3b23aa7913
commit db877453a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 422 additions and 141 deletions

View file

@ -4,7 +4,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
def index def index
@inboxes = policy_scope(Current.account.inboxes.includes(:channel, :avatar_attachment)) @inboxes = policy_scope(Current.account.inboxes.order_by_id.includes(:channel, :avatar_attachment))
end end
def create def create
@ -23,7 +23,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def update def update
@inbox.update(inbox_update_params.except(:channel)) @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? return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
@inbox.channel.update!(inbox_update_params[:channel])
update_channel_feature_flags
end end
def set_agent_bot def set_agent_bot
@ -67,6 +70,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end end
end end
def update_channel_feature_flags
return unless inbox_update_params[:channel].key? :selected_feature_flags
@inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags]
@inbox.channel.save!
end
def permitted_params def permitted_params
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel: params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email]) [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email])
@ -74,6 +84,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_update_params def inbox_update_params
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled, params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email]) channel: [
:website_url,
:widget_color,
:welcome_title,
:welcome_tagline,
:webhook_url,
:email,
selected_feature_flags: []
])
end end
end end

View file

@ -1,11 +1,11 @@
html, html,
body { body {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
height: 100%;
margin: 0;
padding: 0;
width: 100%;
} }
.app-wrapper { .app-wrapper {
@ -26,36 +26,40 @@ body {
.view-box { .view-box {
@include full-height; @include full-height;
height: 100vh;
@include margin(0); @include margin(0);
@include space-between-column; @include space-between-column;
height: 100vh;
} }
.view-panel { .view-panel {
flex-direction: column;
@include margin($zero); @include margin($zero);
@include padding($space-normal); @include padding($space-normal);
flex-direction: column;
overflow-y: auto; overflow-y: auto;
} }
.content-box { .content-box {
overflow: auto;
@include padding($space-normal); @include padding($space-normal);
overflow: auto;
} }
.back-button { .back-button {
@include flex; @include flex;
align-items: center; align-items: center;
color: $color-woot; color: $color-woot;
cursor: pointer;
font-size: $font-size-default; font-size: $font-size-default;
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
margin-right: $space-normal; margin-right: $space-normal;
cursor: pointer;
&:before { &::before {
vertical-align: text-bottom;
margin-right: $space-smaller;
font-size: $font-size-large; font-size: $font-size-large;
margin-right: $space-small;
vertical-align: text-bottom;
} }
} }
@ -66,12 +70,14 @@ body {
.no-items-error-message { .no-items-error-message {
@include flex; @include flex;
@include full-height; @include full-height;
justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center;
img { img {
max-width: $space-mega;
@include padding($space-one); @include padding($space-one);
max-width: $space-mega;
} }
} }

View file

@ -202,7 +202,7 @@
} }
.settings--content { .settings--content {
@include margin($space-small $space-larger); @include margin($space-small $space-large);
.title { .title {
font-weight: $font-weight-medium; font-weight: $font-weight-medium;

View file

@ -7,15 +7,25 @@
<p v-if="headerContent" class="small-12 column"> <p v-if="headerContent" class="small-12 column">
{{ headerContent }} {{ headerContent }}
</p> </p>
<slot></slot>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
headerTitle: String, headerTitle: {
headerContent: String, type: String,
headerImage: String, default: '',
},
headerContent: {
type: String,
default: '',
},
headerImage: {
type: String,
default: '',
},
}, },
}; };
</script> </script>

View file

@ -57,15 +57,8 @@ import { mapGetters } from 'vuex';
import router from '../../routes'; import router from '../../routes';
import adminMixin from '../../mixins/isAdmin'; import adminMixin from '../../mixins/isAdmin';
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
TWILIO: 'Channel::TwilioSms',
API: 'Channel::Api',
EMAIL: 'Channel::Email',
};
const getInboxClassByType = type => { const getInboxClassByType = type => {
switch (type) { switch (type) {
case INBOX_TYPES.WEB: case INBOX_TYPES.WEB:

View file

@ -1,43 +1,43 @@
<template> <template>
<div <div
class="small-3 columns channel" class="small-3 columns channel"
:class="{ inactive: !isActive(channel) }" :class="{ inactive: !isActive }"
@click="onItemClick" @click="onItemClick"
> >
<img <img
v-if="channel === 'facebook'" v-if="channel.key === 'facebook'"
src="~dashboard/assets/images/channels/facebook.png" src="~dashboard/assets/images/channels/facebook.png"
/> />
<img <img
v-if="channel === 'twitter'" v-if="channel.key === 'twitter'"
src="~dashboard/assets/images/channels/twitter.png" src="~dashboard/assets/images/channels/twitter.png"
/> />
<img <img
v-if="channel === 'telegram'" v-if="channel.key === 'telegram'"
src="~dashboard/assets/images/channels/telegram.png" src="~dashboard/assets/images/channels/telegram.png"
/> />
<img <img
v-if="channel === 'api'" v-if="channel.key === 'api'"
src="~dashboard/assets/images/channels/api.png" src="~dashboard/assets/images/channels/api.png"
/> />
<img <img
v-if="channel === 'email'" v-if="channel.key === 'email'"
src="~dashboard/assets/images/channels/email.png" src="~dashboard/assets/images/channels/email.png"
/> />
<img <img
v-if="channel === 'line'" v-if="channel.key === 'line'"
src="~dashboard/assets/images/channels/line.png" src="~dashboard/assets/images/channels/line.png"
/> />
<img <img
v-if="channel === 'website'" v-if="channel.key === 'website'"
src="~dashboard/assets/images/channels/website.png" src="~dashboard/assets/images/channels/website.png"
/> />
<img <img
v-if="channel === 'twilio'" v-if="channel.key === 'twilio'"
src="~dashboard/assets/images/channels/twilio.png" src="~dashboard/assets/images/channels/twilio.png"
/> />
<h3 class="channel__title"> <h3 class="channel__title">
{{ channel }} {{ channel.name }}
</h3> </h3>
</div> </div>
</template> </template>
@ -45,7 +45,7 @@
export default { export default {
props: { props: {
channel: { channel: {
type: String, type: Object,
required: true, required: true,
}, },
enabledFeatures: { enabledFeatures: {
@ -53,25 +53,28 @@ export default {
required: true, required: true,
}, },
}, },
methods: { computed: {
isActive(channel) { isActive() {
const { key } = this.channel;
if (Object.keys(this.enabledFeatures) === 0) { if (Object.keys(this.enabledFeatures) === 0) {
return false; return false;
} }
if (channel === 'facebook') { if (key === 'facebook') {
return this.enabledFeatures.channel_facebook; return this.enabledFeatures.channel_facebook;
} }
if (channel === 'twitter') { if (key === 'twitter') {
return this.enabledFeatures.channel_twitter; return this.enabledFeatures.channel_twitter;
} }
if (channel === 'email') { if (key === 'email') {
return this.enabledFeatures.channel_email; return this.enabledFeatures.channel_email;
} }
return ['website', 'twilio', 'api'].includes(channel); return ['website', 'twilio', 'api'].includes(key);
}, },
},
methods: {
onItemClick() { onItemClick() {
if (this.isActive(this.channel)) { if (this.isActive) {
this.$emit('channel-item-click', this.channel); this.$emit('channel-item-click', this.channel.key);
} }
}, },
}, },

View file

@ -96,6 +96,7 @@ import {
hasPressedShift, hasPressedShift,
} from 'shared/helpers/KeyboardHelpers'; } from 'shared/helpers/KeyboardHelpers';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
export default { export default {
components: { components: {
@ -104,7 +105,7 @@ export default {
FileUpload, FileUpload,
ResizableTextArea, ResizableTextArea,
}, },
mixins: [clickaway], mixins: [clickaway, inboxMixin],
data() { data() {
return { return {
message: '', message: '',
@ -148,9 +149,6 @@ export default {
this.message.length > this.maxLength this.message.length > this.maxLength
); );
}, },
channelType() {
return this.inbox.channel_type;
},
conversationType() { conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat; const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : ''; const type = additionalAttributes ? additionalAttributes.type : '';
@ -174,29 +172,6 @@ export default {
} }
return MESSAGE_MAX_LENGTH.GENERAL; return MESSAGE_MAX_LENGTH.GENERAL;
}, },
isATwitterInbox() {
return this.channelType === 'Channel::TwitterProfile';
},
isAFacebookInbox() {
return this.channelType === 'Channel::FacebookPage';
},
isAWebWidgetInbox() {
return this.channelType === 'Channel::WebWidget';
},
isATwilioSMSChannel() {
const { phone_number: phoneNumber = '' } = this.inbox;
return (
this.channelType === 'Channel::TwilioSms' &&
!phoneNumber.startsWith('whatsapp')
);
},
isATwilioWhatsappChannel() {
const { phone_number: phoneNumber = '' } = this.inbox;
return (
this.channelType === 'Channel::TwilioSms' &&
phoneNumber.startsWith('whatsapp')
);
},
showFileUpload() { showFileUpload() {
return ( return (
this.isAWebWidgetInbox || this.isAWebWidgetInbox ||

View file

@ -117,16 +117,16 @@
}, },
"API_CHANNEL": { "API_CHANNEL": {
"TITLE": "API Channel", "TITLE": "API Channel",
"DESC": "Integrate with API channel and start supporting your customers via chatwoot.", "DESC": "Integrate with API channel and start supporting your customers.",
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Channel Name", "LABEL": "Channel Name",
"PLACEHOLDER": "Please enter a channel name", "PLACEHOLDER": "Please enter a channel name",
"ERROR": "This field is required" "ERROR": "This field is required"
}, },
"WEBHOOK_URL": { "WEBHOOK_URL": {
"LABEL": "Webhook Url", "LABEL": "Webhook URL",
"SUBTITLE": "Configure the url where you want to recieve callbacks from chatwoot on events.", "SUBTITLE": "Configure the URL where you want to recieve callbacks on events.",
"PLACEHOLDER": "Webhook Url" "PLACEHOLDER": "Webhook URL"
}, },
"SUBMIT_BUTTON": "Create API Channel", "SUBMIT_BUTTON": "Create API Channel",
"API": { "API": {
@ -135,7 +135,7 @@
}, },
"EMAIL_CHANNEL": { "EMAIL_CHANNEL": {
"TITLE": "Email Channel", "TITLE": "Email Channel",
"DESC": "Integrate you email inbox with chatwoot.", "DESC": "Integrate you email inbox.",
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Channel Name", "LABEL": "Channel Name",
"PLACEHOLDER": "Please enter a channel name", "PLACEHOLDER": "Please enter a channel name",
@ -150,7 +150,7 @@
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the email channel" "ERROR_MESSAGE": "We were not able to save the email channel"
}, },
"FINISH_MESSAGE" : "Start forwarding your emails to the following email address." "FINISH_MESSAGE": "Start forwarding your emails to the following email address."
}, },
"AUTH": { "AUTH": {
"TITLE": "Channels", "TITLE": "Channels",
@ -212,7 +212,17 @@
"ERROR_MESSAGE": "Could not delete inbox. Please try again later." "ERROR_MESSAGE": "Could not delete inbox. Please try again later."
} }
}, },
"TABS": {
"SETTINGS": "Settings",
"COLLABORATORS": "Collaborators",
"CONFIGURATION": "Configuration"
},
"SETTINGS": "Settings", "SETTINGS": "Settings",
"FEATURES": {
"LABEL": "Features",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget"
},
"SETTINGS_POPUP": { "SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script", "MESSENGER_HEADING": "Messenger Script",
"MESSENGER_SUB_HEAD": "Place this button inside your body tag", "MESSENGER_SUB_HEAD": "Place this button inside your body tag",

View file

@ -15,7 +15,7 @@
v-model="selectedAgents" v-model="selectedAgents"
:options="agentList" :options="agentList"
track-by="id" track-by="id"
label="name" label="available_name"
:multiple="true" :multiple="true"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="false"

View file

@ -30,14 +30,14 @@ export default {
data() { data() {
return { return {
channelList: [ channelList: [
'website', { key: 'website', name: 'Website' },
'facebook', { key: 'facebook', name: 'Facebook' },
'twitter', { key: 'twitter', name: 'Twitter' },
'twilio', { key: 'twilio', name: 'Twilio' },
'email', { key: 'email', name: 'Email' },
'api', { key: 'api', name: 'API' },
'telegram', { key: 'telegram', name: 'Telegram' },
'line', { key: 'line', name: 'Line' },
], ],
enabledFeatures: {}, enabledFeatures: {},
}; };

View file

@ -3,9 +3,18 @@
<woot-modal-header <woot-modal-header
:header-image="inbox.avatarUrl" :header-image="inbox.avatarUrl"
:header-title="inboxName" :header-title="inboxName"
/> >
<woot-tabs :index="selectedTabIndex" @change="onTabChange">
<woot-tabs-item
v-for="tab in tabs"
:key="tab.key"
:name="tab.name"
:show-badge="false"
/>
</woot-tabs>
</woot-modal-header>
<div class="settings--content"> <div v-if="selectedTabKey === 'inbox_settings'" class="settings--content">
<settings-section <settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')" :title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')" :sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')"
@ -16,7 +25,7 @@
@change="handleImageUpload" @change="handleImageUpload"
/> />
<woot-input <woot-input
v-if="isAWidgetInbox" v-if="isAWebWidgetInbox"
v-model.trim="selectedInboxName" v-model.trim="selectedInboxName"
class="medium-9 columns" class="medium-9 columns"
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL')" :label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL')"
@ -25,7 +34,7 @@
" "
/> />
<woot-input <woot-input
v-if="isAWidgetInbox" v-if="isAWebWidgetInbox"
v-model.trim="channelWebsiteUrl" v-model.trim="channelWebsiteUrl"
class="medium-9 columns" class="medium-9 columns"
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL')" :label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL')"
@ -34,7 +43,7 @@
" "
/> />
<woot-input <woot-input
v-if="isAWidgetInbox" v-if="isAWebWidgetInbox"
v-model.trim="channelWelcomeTitle" v-model.trim="channelWelcomeTitle"
class="medium-9 columns" class="medium-9 columns"
:label=" :label="
@ -48,7 +57,7 @@
/> />
<woot-input <woot-input
v-if="isAWidgetInbox" v-if="isAWebWidgetInbox"
v-model.trim="channelWelcomeTagline" v-model.trim="channelWelcomeTagline"
class="medium-9 columns" class="medium-9 columns"
:label=" :label="
@ -61,7 +70,7 @@
" "
/> />
<label v-if="isAWidgetInbox" class="medium-9 columns"> <label v-if="isAWebWidgetInbox" class="medium-9 columns">
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL') }} {{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL') }}
<woot-color-picker v-model="inbox.widget_color" /> <woot-color-picker v-model="inbox.widget_color" />
</label> </label>
@ -94,7 +103,6 @@
}} }}
</p> </p>
</label> </label>
<woot-input <woot-input
v-if="greetingEnabled" v-if="greetingEnabled"
v-model.trim="greetingMessage" v-model.trim="greetingMessage"
@ -122,6 +130,33 @@
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }} {{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
</p> </p>
</label> </label>
<label v-if="isAWebWidgetInbox">
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
</label>
<div v-if="isAWebWidgetInbox" class="widget--feature-flag">
<input
v-model="selectedFeatureFlags"
type="checkbox"
value="attachments"
@input="handleFeatureFlag"
/>
<label for="attachments">
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_FILE_PICKER') }}
</label>
</div>
<div v-if="isAWebWidgetInbox">
<input
v-model="selectedFeatureFlags"
type="checkbox"
value="emoji_picker"
@input="handleFeatureFlag"
/>
<label for="emoji_picker">
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_EMOJI_PICKER') }}
</label>
</div>
<woot-submit-button <woot-submit-button
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')" :button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:loading="uiFlags.isUpdatingInbox" :loading="uiFlags.isUpdatingInbox"
@ -132,7 +167,7 @@
<!-- update agents in inbox --> <!-- update agents in inbox -->
<div class="settings--content"> <div v-if="selectedTabKey === 'collaborators'" class="settings--content">
<settings-section <settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')" :title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')" :sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
@ -141,7 +176,7 @@
v-model="selectedAgents" v-model="selectedAgents"
:options="agentList" :options="agentList"
track-by="id" track-by="id"
label="name" label="available_name"
:multiple="true" :multiple="true"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="false"
@ -157,56 +192,43 @@
/> />
</settings-section> </settings-section>
</div> </div>
<div v-if="selectedTabKey === 'configuration'">
<div <div v-if="isATwilioChannel" class="settings--content">
v-if="inbox.channel_type === 'Channel::TwilioSms'"
class="settings--content"
>
<settings-section
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
>
<woot-code :script="twilioCallbackURL" lang="html"></woot-code>
</settings-section>
</div>
<div
v-if="inbox.channel_type === 'Channel::FacebookPage'"
class="settings--content"
>
<settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
>
<woot-code :script="messengerScript"></woot-code>
</settings-section>
</div>
<div v-else-if="inbox.channel_type === 'Channel::WebWidget'">
<div class="settings--content">
<settings-section <settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')" :title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')" :sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
> >
<woot-code :script="inbox.web_widget_script"></woot-code> <woot-code :script="twilioCallbackURL" lang="html"></woot-code>
</settings-section> </settings-section>
</div> </div>
<div v-else-if="isAWebWidgetInbox">
<div class="settings--content">
<settings-section
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
>
<woot-code :script="inbox.web_widget_script"></woot-code>
</settings-section>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { createMessengerScript } from 'dashboard/helper/scriptGenerator'; import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
import configMixin from 'shared/mixins/configMixin'; import configMixin from 'shared/mixins/configMixin';
import alertMixin from 'shared/mixins/alertMixin';
import SettingsSection from '../../../../components/SettingsSection'; import SettingsSection from '../../../../components/SettingsSection';
import inboxMixin from 'shared/mixins/inboxMixin';
export default { export default {
components: { components: {
SettingsSection, SettingsSection,
}, },
mixins: [configMixin], mixins: [alertMixin, configMixin, inboxMixin],
data() { data() {
return { return {
avatarFile: null, avatarFile: null,
@ -220,6 +242,7 @@ export default {
channelWebsiteUrl: '', channelWebsiteUrl: '',
channelWelcomeTitle: '', channelWelcomeTitle: '',
channelWelcomeTagline: '', channelWelcomeTagline: '',
selectedFeatureFlags: [],
autoAssignmentOptions: [ autoAssignmentOptions: [
{ {
value: true, value: true,
@ -230,6 +253,7 @@ export default {
label: this.$t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.DISABLED'), label: this.$t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.DISABLED'),
}, },
], ],
selectedTabIndex: 0,
}; };
}, },
computed: { computed: {
@ -237,17 +261,41 @@ export default {
agentList: 'agents/getAgents', agentList: 'agents/getAgents',
uiFlags: 'inboxes/getUIFlags', uiFlags: 'inboxes/getUIFlags',
}), }),
selectedTabKey() {
return this.tabs[this.selectedTabIndex]?.key;
},
tabs() {
const visibleToAllChannelTabs = [
{
key: 'inbox_settings',
name: this.$t('INBOX_MGMT.TABS.SETTINGS'),
},
{
key: 'collaborators',
name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'),
},
];
if (this.isAWebWidgetInbox || this.isATwilioChannel) {
return [
...visibleToAllChannelTabs,
{
key: 'configuration',
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
},
];
}
return visibleToAllChannelTabs;
},
currentInboxId() { currentInboxId() {
return this.$route.params.inboxId; return this.$route.params.inboxId;
}, },
inbox() { inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId); return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
}, },
isAWidgetInbox() {
return this.inbox.channel_type === 'Channel::WebWidget';
},
inboxName() { inboxName() {
if (this.inbox.channel_type === 'Channel::TwilioSms') { if (this.isATwilioSMSChannel || this.isATwilioWhatsappChannel) {
return `${this.inbox.name} (${this.inbox.phone_number})`; return `${this.inbox.name} (${this.inbox.phone_number})`;
} }
return this.inbox.name; return this.inbox.name;
@ -267,10 +315,25 @@ export default {
this.fetchInboxSettings(); this.fetchInboxSettings();
}, },
methods: { methods: {
showAlert(message) { handleFeatureFlag(e) {
bus.$emit('newToastMessage', message); console.log(e.target.value);
this.selectedFeatureFlags = this.toggleInput(
this.selectedFeatureFlags,
e.target.value
);
},
toggleInput(selected, current) {
if (selected.includes(current)) {
const newSelectedFlags = selected.filter(flag => flag !== current);
return newSelectedFlags;
}
return [...selected, current];
},
onTabChange(selectedTabIndex) {
this.selectedTabIndex = selectedTabIndex;
}, },
fetchInboxSettings() { fetchInboxSettings() {
this.selectedTabIndex = 0;
this.selectedAgents = []; this.selectedAgents = [];
this.$store.dispatch('agents/get'); this.$store.dispatch('agents/get');
this.$store.dispatch('inboxes/get').then(() => { this.$store.dispatch('inboxes/get').then(() => {
@ -283,6 +346,7 @@ export default {
this.channelWebsiteUrl = this.inbox.website_url; this.channelWebsiteUrl = this.inbox.website_url;
this.channelWelcomeTitle = this.inbox.welcome_title; this.channelWelcomeTitle = this.inbox.welcome_title;
this.channelWelcomeTagline = this.inbox.welcome_tagline; this.channelWelcomeTagline = this.inbox.welcome_tagline;
this.selectedFeatureFlags = this.inbox.selected_feature_flags || [];
}); });
}, },
async fetchAttachedAgents() { async fetchAttachedAgents() {
@ -325,6 +389,7 @@ export default {
website_url: this.channelWebsiteUrl, website_url: this.channelWebsiteUrl,
welcome_title: this.channelWelcomeTitle || '', welcome_title: this.channelWelcomeTitle || '',
welcome_tagline: this.channelWelcomeTagline || '', welcome_tagline: this.channelWelcomeTagline || '',
selectedFeatureFlags: this.selectedFeatureFlags,
}, },
}; };
if (this.avatarFile) { if (this.avatarFile) {
@ -370,7 +435,16 @@ export default {
.page-top-bar { .page-top-bar {
@include background-light; @include background-light;
@include border-normal-bottom; @include border-normal-bottom;
padding: $space-normal $space-larger; padding: $space-normal $space-large 0;
.tabs {
padding: 0;
margin-bottom: -1px;
}
} }
} }
.widget--feature-flag {
padding-top: var(--space-small);
}
</style> </style>

View file

@ -11,7 +11,15 @@ const buildInboxData = inboxParams => {
Object.keys(inboxProperties).forEach(key => { Object.keys(inboxProperties).forEach(key => {
formData.append(key, inboxProperties[key]); formData.append(key, inboxProperties[key]);
}); });
Object.keys(channel).forEach(key => { const { selectedFeatureFlags = [], ...channelParams } = channel;
if (selectedFeatureFlags.length) {
selectedFeatureFlags.forEach(featureFlag => {
formData.append(`channel[selected_feature_flags][]`, featureFlag);
});
} else {
formData.append('channel[selected_feature_flags][]', '');
}
Object.keys(channelParams).forEach(key => {
formData.append(`channel[${key}]`, channel[key]); formData.append(`channel[${key}]`, channel[key]);
}); });
return formData; return formData;

View file

@ -0,0 +1,42 @@
export const INBOX_TYPES = {
WEB: 'Channel::WebWidget',
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
TWILIO: 'Channel::TwilioSms',
API: 'Channel::Api',
EMAIL: 'Channel::Email',
};
export default {
computed: {
channelType() {
return this.inbox.channel_type;
},
isAPIInbox() {
return this.channelType === INBOX_TYPES.API;
},
isATwitterInbox() {
return this.channelType === INBOX_TYPES.TWITTER;
},
isAFacebookInbox() {
return this.channelType === INBOX_TYPES.FB;
},
isAWebWidgetInbox() {
return this.channelType === INBOX_TYPES.WEB;
},
isATwilioChannel() {
return this.channelType === INBOX_TYPES.TWILIO;
},
isAnEmailChannel() {
return this.channelType === INBOX_TYPES.EMAIL;
},
isATwilioSMSChannel() {
const { phone_number: phoneNumber = '' } = this.inbox;
return this.isATwilioChannel && !phoneNumber.startsWith('whatsapp');
},
isATwilioWhatsappChannel() {
const { phone_number: phoneNumber = '' } = this.inbox;
return this.isATwilioChannel && phoneNumber.startsWith('whatsapp');
},
},
};

View file

@ -0,0 +1,114 @@
import { shallowMount } from '@vue/test-utils';
import inboxMixin from '../inboxMixin';
describe('inboxMixin', () => {
it('returns the correct channel type', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return { inbox: { channel_type: 'Channel::WebWidget' } };
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.channelType).toBe('Channel::WebWidget');
});
it('isAPIInbox returns true if channel type is API', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return { inbox: { channel_type: 'Channel::Api' } };
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isAPIInbox).toBe(true);
});
it('isATwitterInbox returns true if channel type is twitter', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return { inbox: { channel_type: 'Channel::TwitterProfile' } };
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isATwitterInbox).toBe(true);
});
it('isAFacebookInbox returns true if channel type is Facebook', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return { inbox: { channel_type: 'Channel::FacebookPage' } };
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isAFacebookInbox).toBe(true);
});
it('isAWebWidgetInbox returns true if channel type is Facebook', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return { inbox: { channel_type: 'Channel::WebWidget' } };
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isAWebWidgetInbox).toBe(true);
});
it('isATwilioChannel returns true if channel type is Twilio', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return {
inbox: {
channel_type: 'Channel::TwilioSms',
phone_number: '+91944444444',
},
};
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isATwilioChannel).toBe(true);
expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
});
it('isATwilioWhatsappChannel returns true if channel type is Twilio and phonenumber is a whatsapp number', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return {
inbox: {
channel_type: 'Channel::TwilioSms',
phone_number: 'whatsapp:+91944444444',
},
};
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isATwilioChannel).toBe(true);
expect(wrapper.vm.isATwilioWhatsappChannel).toBe(true);
});
it('isAnEmailChannel returns true if channel type is email', () => {
const Component = {
render() {},
mixins: [inboxMixin],
data() {
return {
inbox: { channel_type: 'Channel::Email' },
};
},
};
const wrapper = shallowMount(Component);
expect(wrapper.vm.isAnEmailChannel).toBe(true);
});
});

View file

@ -18,6 +18,7 @@
:on-click="emojiOnClick" :on-click="emojiOnClick"
/> />
<i <i
v-if="hasEmojiPickerEnabled"
class="emoji-toggle icon ion-happy-outline" class="emoji-toggle icon ion-happy-outline"
:class="{ active: showEmojiPicker }" :class="{ active: showEmojiPicker }"
@click="toggleEmojiPicker()" @click="toggleEmojiPicker()"
@ -39,6 +40,7 @@ import ChatSendButton from 'widget/components/ChatSendButton.vue';
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue'; import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea'; import ResizableTextArea from 'shared/components/ResizableTextArea';
import EmojiInput from 'dashboard/components/widgets/emoji/EmojiInput'; import EmojiInput from 'dashboard/components/widgets/emoji/EmojiInput';
import configMixin from '../mixins/configMixin';
export default { export default {
name: 'ChatInputWrap', name: 'ChatInputWrap',
@ -48,7 +50,7 @@ export default {
EmojiInput, EmojiInput,
ResizableTextArea, ResizableTextArea,
}, },
mixins: [clickaway], mixins: [clickaway, configMixin],
props: { props: {
onSendMessage: { onSendMessage: {
type: Function, type: Function,
@ -72,7 +74,7 @@ export default {
widgetColor: 'appConfig/getWidgetColor', widgetColor: 'appConfig/getWidgetColor',
}), }),
showAttachment() { showAttachment() {
return this.userInput.length === 0; return this.hasAttachmentsEnabled && this.userInput.length === 0;
}, },
showSendButton() { showSendButton() {
return this.userInput.length > 0; return this.userInput.length > 0;

View file

@ -15,5 +15,11 @@ export default {
channelConfig() { channelConfig() {
return window.chatwootWebChannel; return window.chatwootWebChannel;
}, },
hasEmojiPickerEnabled() {
return this.channelConfig.enabledFeatures.includes('emoji_picker');
},
hasAttachmentsEnabled() {
return this.channelConfig.enabledFeatures.includes('attachments');
},
}, },
}; };

View file

@ -6,6 +6,7 @@ global.chatwootWebChannel = {
hideInputForBotConversations: true, hideInputForBotConversations: true,
avatarUrl: 'https://test.url', avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot', hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: ['emoji_picker', 'attachments'],
}; };
global.chatwootWidgetDefaults = { global.chatwootWidgetDefaults = {
@ -22,6 +23,8 @@ describe('configMixin', () => {
const Constructor = Vue.extend(Component); const Constructor = Vue.extend(Component);
const vm = new Constructor().$mount(); const vm = new Constructor().$mount();
const wrapper = createWrapper(vm); const wrapper = createWrapper(vm);
expect(wrapper.vm.hasEmojiPickerEnabled).toBe(true);
expect(wrapper.vm.hasAttachmentsEnabled).toBe(true);
expect(wrapper.vm.hideInputForBotConversations).toBe(true); expect(wrapper.vm.hideInputForBotConversations).toBe(true);
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true); expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
expect(wrapper.vm.useInboxAvatarForBot).toBe(true); expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
@ -30,6 +33,7 @@ describe('configMixin', () => {
hideInputForBotConversations: true, hideInputForBotConversations: true,
avatarUrl: 'https://test.url', avatarUrl: 'https://test.url',
hasAConnectedAgentBot: 'AgentBot', hasAConnectedAgentBot: 'AgentBot',
enabledFeatures: ['emoji_picker', 'attachments'],
}); });
}); });
}); });

View file

@ -3,6 +3,7 @@
# 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
# website_token :string # website_token :string
# website_url :string # website_url :string
# welcome_tagline :string # welcome_tagline :string
@ -18,6 +19,8 @@
# #
class Channel::WebWidget < ApplicationRecord class Channel::WebWidget < ApplicationRecord
include FlagShihTzu
self.table_name = 'channel_web_widgets' self.table_name = 'channel_web_widgets'
validates :website_url, presence: true validates :website_url, presence: true
@ -26,6 +29,9 @@ class Channel::WebWidget < ApplicationRecord
belongs_to :account belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy has_one :inbox, as: :channel, dependent: :destroy
has_secure_token :website_token has_secure_token :website_token
has_flags 1 => :attachments,
2 => :emoji_picker,
:column => 'feature_flags'
def has_24_hour_messaging_window? def has_24_hour_messaging_window?
false false

View file

@ -46,6 +46,8 @@ class Inbox < ApplicationRecord
after_destroy :delete_round_robin_agents after_destroy :delete_round_robin_agents
scope :order_by_id, -> { order(id: :asc) }
def add_member(user_id) def add_member(user_id)
member = inbox_members.new(user_id: user_id) member = inbox_members.new(user_id: user_id)
member.save! member.save!

View file

@ -14,3 +14,4 @@ json.enable_auto_assignment resource.enable_auto_assignment
json.web_widget_script resource.channel.try(:web_widget_script) json.web_widget_script resource.channel.try(:web_widget_script)
json.forward_to_address resource.channel.try(:forward_to_address) json.forward_to_address resource.channel.try(:forward_to_address)
json.phone_number resource.channel.try(:phone_number) json.phone_number resource.channel.try(:phone_number)
json.selected_feature_flags resource.channel.try(:selected_feature_flags)

View file

@ -17,6 +17,7 @@
welcomeTagline: '<%= @web_widget.welcome_tagline %>', welcomeTagline: '<%= @web_widget.welcome_tagline %>',
welcomeTitle: '<%= @web_widget.welcome_title %>', welcomeTitle: '<%= @web_widget.welcome_title %>',
widgetColor: '<%= @web_widget.widget_color %>', widgetColor: '<%= @web_widget.widget_color %>',
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 %>,
} }
window.chatwootWidgetDefaults = { window.chatwootWidgetDefaults = {

View file

@ -0,0 +1,5 @@
class AddFeatureSettingToWebsiteInbox < ActiveRecord::Migration[6.0]
def change
add_column :channel_web_widgets, :feature_flags, :integer, default: 3, null: false
end
end

View file

@ -178,6 +178,7 @@ ActiveRecord::Schema.define(version: 2020_08_02_170002) do
t.string "widget_color", default: "#1f93ff" t.string "widget_color", default: "#1f93ff"
t.string "welcome_title" t.string "welcome_title"
t.string "welcome_tagline" t.string "welcome_tagline"
t.integer "feature_flags", default: 3, null: false
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