feat: Widget Builder (#5104)

* feat: Add widget builder
This commit is contained in:
Aswin Dev P.S 2022-08-02 13:04:20 +05:30 committed by GitHub
parent 945288ce15
commit d9b102cff0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1079 additions and 149 deletions

View file

@ -0,0 +1,3 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63 63H32.9976C16.4591 63 2.99996 49.5399 2.99996 32.9973C2.99996 16.4601 16.4591 3 32.9979 3C49.5408 3 63 16.4601 63 32.9973V63Z" fill="white" stroke="white" stroke-width="6"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -413,7 +413,8 @@
"CONFIGURATION": "Configuration", "CONFIGURATION": "Configuration",
"CAMPAIGN": "Campaigns", "CAMPAIGN": "Campaigns",
"PRE_CHAT_FORM": "Pre Chat Form", "PRE_CHAT_FORM": "Pre Chat Form",
"BUSINESS_HOURS": "Business Hours" "BUSINESS_HOURS": "Business Hours",
"WIDGET_BUILDER": "Widget Builder"
}, },
"SETTINGS": "Settings", "SETTINGS": "Settings",
"FEATURES": { "FEATURES": {
@ -574,6 +575,88 @@
"OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode", "OPEN_SSL_VERIFY_MODE": "Open SSL Verify Mode",
"AUTH_MECHANISM": "Authentication" "AUTH_MECHANISM": "Authentication"
}, },
"NOTE": "Note: " "NOTE": "Note: ",
"WIDGET_BUILDER": {
"WIDGET_OPTIONS":{
"AVATAR":{
"LABEL": "Website Avatar",
"DELETE":{
"API":{
"SUCCESS_MESSAGE": "Avatar deleted successfully",
"ERROR_MESSAGE": "There was an error, please try again"
}
}
},
"WEBSITE_NAME": {
"LABEL": "Website Name",
"PLACE_HOLDER": "Enter your website name (eg: Acme Inc)",
"ERROR": "Please enter a valid website name"
},
"WELCOME_HEADING": {
"LABEL": "Welcome Heading",
"PLACE_HOLDER": "Hi there!"
},
"WELCOME_TAGLINE": {
"LABEL": "Welcome Tagline",
"PLACE_HOLDER": "We make it simple to connect with us. Ask us anything, or share your feedback."
},
"REPLY_TIME": {
"LABEL": "Reply Time",
"IN_A_FEW_MINUTES": "In a few minutes",
"IN_A_FEW_HOURS": "In a few hours",
"IN_A_DAY": "In a day"
},
"WIDGET_COLOR_LABEL": "Widget Color",
"WIDGET_BUBBLE_POSITION_LABEL": "Widget Bubble Position",
"WIDGET_BUBBLE_TYPE_LABEL": "Widget Bubble Type",
"WIDGET_BUBBLE_LAUNCHER_TITLE":{
"DEFAULT": "Chat with us",
"LABEL": "Widget Bubble Launcher Title",
"PLACE_HOLDER": "Chat with us"
},
"UPDATE": {
"BUTTON_TEXT": "Update Widget Settings",
"API": {
"SUCCESS_MESSAGE": "Widget settings updated successfully",
"ERROR_MESSAGE": "Unable to update widget settings"
}
},
"WIDGET_VIEW_OPTION":{
"PREVIEW": "Preview",
"SCRIPT": "Script"
},
"WIDGET_BUBBLE_POSITION":{
"LEFT": "Left",
"RIGHT": "Right"
},
"WIDGET_BUBBLE_TYPE":{
"STANDARD": "Standard",
"EXPANDED_BUBBLE": "Expanded Bubble"
}
},
"WIDGET_SCREEN": {
"DEFAULT": "Default",
"CHAT": "Chat"
},
"REPLY_TIME": {
"IN_A_FEW_MINUTES": "Typically replies in a few minutes",
"IN_A_FEW_HOURS": "Typically replies in a few hours",
"IN_A_DAY": "Typically replies in a day"
},
"FOOTER": {
"START_CONVERSATION_BUTTON_TEXT": "Start Conversation",
"CHAT_INPUT_PLACEHOLDER": "Type your message"
},
"BODY": {
"TEAM_AVAILABILITY":{
"ONLINE": "We are Online",
"OFFLINE": "We are away at the moment"
},
"USER_MESSAGE": "Hi",
"AGENT_MESSAGE": "Hello"
},
"BRANDING_TEXT": "Powered by Chatwoot",
"SCRIPT_SETTINGS": "\n window.chatwootSettings = {options};"
}
} }
} }

View file

@ -1,8 +1,42 @@
<template> <template>
<div class="widget-wrapper"> <div class="widget-preview-container">
<div v-if="isWidgetVisible" class="screen-selector">
<input-radio-group
name="widget-screen"
:items="widgetScreens"
:action="handleScreenChange"
/>
</div>
<div v-if="isWidgetVisible" class="widget-wrapper">
<WidgetHead :config="getWidgetHeadConfig" /> <WidgetHead :config="getWidgetHeadConfig" />
<WidgetBody /> <div>
<WidgetFooter /> <WidgetBody :config="getWidgetBodyConfig" />
<WidgetFooter :config="getWidgetFooterConfig" />
<div class="branding">
<a class="branding-link">
<img class="branding-image" :src="globalConfig.logoThumbnail" />
<span>{{ $t('INBOX_MGMT.WIDGET_BUILDER.BRANDING_TEXT') }}</span>
</a>
</div>
</div>
</div>
<div class="widget-bubble" :style="getBubblePositionStyle">
<button
class="bubble"
:class="getBubbleTypeClass"
:style="{ background: color }"
@click="toggleWidget"
>
<img
v-if="!isWidgetVisible"
src="~dashboard/assets/images/bubble-logo.svg"
alt=""
/>
<div>
{{ getWidgetBubbleLauncherTitle }}
</div>
</button>
</div>
</div> </div>
</template> </template>
@ -10,6 +44,9 @@
import WidgetHead from './WidgetHead'; import WidgetHead from './WidgetHead';
import WidgetBody from './WidgetBody'; import WidgetBody from './WidgetBody';
import WidgetFooter from './WidgetFooter'; import WidgetFooter from './WidgetFooter';
import InputRadioGroup from 'dashboard/routes/dashboard/settings/inbox/components/InputRadioGroup';
const { LOGO_THUMBNAIL: logoThumbnail } = window.globalConfig || {};
export default { export default {
name: 'Widget', name: 'Widget',
@ -17,13 +54,14 @@ export default {
WidgetHead, WidgetHead,
WidgetBody, WidgetBody,
WidgetFooter, WidgetFooter,
InputRadioGroup,
}, },
props: { props: {
welcomeHeading: { welcomeHeading: {
type: String, type: String,
default: 'Hi There,', default: '',
}, },
welcomeTagLine: { welcomeTagline: {
type: String, type: String,
default: '', default: '',
}, },
@ -32,52 +70,228 @@ export default {
default: '', default: '',
required: true, required: true,
}, },
websiteDomain: {
type: String,
default: '',
},
logo: { logo: {
type: String, type: String,
default: '', default: '',
}, },
isExpanded: {
type: Boolean,
default: true,
},
isOnline: { isOnline: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
replyTime: { replyTime: {
type: String, type: String,
default: 'few minutes', default: '',
}, },
color: {
type: String,
default: '',
},
widgetBubblePosition: {
type: String,
default: '',
},
widgetBubbleLauncherTitle: {
type: String,
default: '',
},
widgetBubbleType: {
type: String,
default: '',
},
},
data() {
return {
widgetScreens: [
{
id: 'default',
title: this.$t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_SCREEN.DEFAULT'),
checked: true,
},
{
id: 'chat',
title: this.$t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_SCREEN.CHAT'),
checked: false,
},
],
isDefaultScreen: true,
isWidgetVisible: true,
globalConfig: {
logoThumbnail,
},
};
}, },
computed: { computed: {
getWidgetHeadConfig() { getWidgetHeadConfig() {
return { return {
welcomeHeading: this.welcomeHeading, welcomeHeading: this.welcomeHeading,
welcomeTagLine: this.welcomeTagLine, welcomeTagline: this.welcomeTagline,
websiteName: this.websiteName, websiteName: this.websiteName,
websiteDomain: this.websiteDomain,
logo: this.logo, logo: this.logo,
isExpanded: this.isExpanded, isDefaultScreen: this.isDefaultScreen,
isOnline: this.isOnline, isOnline: this.isOnline,
replyTime: this.replyTime, replyTime: this.replyTimeText,
color: this.color,
}; };
}, },
getWidgetBodyConfig() {
return {
welcomeHeading: this.welcomeHeading,
welcomeTagline: this.welcomeTagline,
isDefaultScreen: this.isDefaultScreen,
isOnline: this.isOnline,
replyTime: this.replyTimeText,
color: this.color,
logo: this.logo,
};
},
getWidgetFooterConfig() {
return {
isDefaultScreen: this.isDefaultScreen,
color: this.color,
};
},
replyTimeText() {
switch (this.replyTime) {
case 'in_a_few_minutes':
return this.$t(
'INBOX_MGMT.WIDGET_BUILDER.REPLY_TIME.IN_A_FEW_MINUTES'
);
case 'in_a_day':
return this.$t('INBOX_MGMT.WIDGET_BUILDER.REPLY_TIME.IN_A_DAY');
default:
return this.$t('INBOX_MGMT.WIDGET_BUILDER.REPLY_TIME.IN_A_FEW_HOURS');
}
},
getBubblePositionStyle() {
return {
justifyContent: this.widgetBubblePosition === 'left' ? 'start' : 'end',
};
},
getBubbleTypeClass() {
return {
'bubble-close': this.isWidgetVisible,
'bubble-expanded':
!this.isWidgetVisible && this.widgetBubbleType === 'expanded_bubble',
};
},
getWidgetBubbleLauncherTitle() {
return this.isWidgetVisible || this.widgetBubbleType === 'standard'
? ' '
: this.widgetBubbleLauncherTitle;
},
},
methods: {
handleScreenChange(item) {
this.isDefaultScreen = item.id === 'default';
},
toggleWidget() {
this.isWidgetVisible = !this.isWidgetVisible;
this.isDefaultScreen = true;
},
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.text-lg { .screen-selector {
font-size: var(--font-size-default); display: flex;
flex-direction: column;
align-items: center;
} }
.widget-wrapper { .widget-wrapper {
box-shadow: var(--shadow-larger); display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: var(--shadow-widget-builder);
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
background-color: var(--color-background); background-color: #f4f6fb;
z-index: 99; width: calc(var(--space-large) * 10);
height: calc(var(--space-mega) * 5);
.branding {
padding-top: var(--space-one);
padding-bottom: var(--space-one);
display: flex;
justify-content: center;
.branding-link {
display: flex;
flex-direction: row;
align-items: center;
color: var(--b-500);
cursor: pointer;
filter: grayscale(1);
font-size: var(--font-size-micro);
opacity: 0.9;
text-decoration: none;
&:hover {
filter: grayscale(0);
opacity: 1;
color: var(--b-600);
}
.branding-image {
max-width: var(--space-one);
max-height: var(--space-one);
margin-right: var(--space-micro);
}
}
}
}
.widget-bubble {
display: flex;
flex-direction: row;
margin-top: var(--space-normal);
width: calc(var(--space-large) * 10);
.bubble {
display: flex;
align-items: center;
border-radius: calc(var(--border-radius-small) * 10);
height: calc(var(--space-three) * 2);
width: calc(var(--space-three) * 2);
position: relative;
overflow-wrap: anywhere;
cursor: pointer;
img {
height: var(--space-two);
width: var(--space-two);
margin: var(--space-one) var(--space-one) var(--space-one)
var(--space-two);
}
div {
padding-right: var(--space-two);
}
}
.bubble-close::before,
.bubble-close::after {
background-color: var(--white);
content: ' ';
display: inline;
height: var(--space-medium);
width: var(--space-micro);
left: var(--space-three);
position: absolute;
}
.bubble-close::before {
transform: rotate(45deg);
}
.bubble-close::after {
transform: rotate(-45deg);
}
.bubble-expanded {
font-size: var(--font-size-default);
font-weight: var(--font-weight-medium);
color: var(--white);
width: auto !important;
height: var(--space-larger) !important;
}
} }
</style> </style>

View file

@ -1,12 +1,27 @@
<template> <template>
<div class="conversation--container"> <div class="widget-body-container">
<div v-if="config.isDefaultScreen" class="availability-content">
<div class="availability-info">
<div class="team-status">
{{ getStatusText }}
</div>
<div class="reply-wait-message">
{{ config.replyTime }}
</div>
</div>
<thumbnail username="J" size="40" />
</div>
<div v-else class="conversation-content">
<div class="conversation-wrap"> <div class="conversation-wrap">
<div class="message-wrap"> <div class="message-wrap">
<div class="user-message-wrap"> <div class="user-message-wrap">
<div class="user-message"> <div class="user-message">
<div class="message-wrap"> <div class="message-wrap">
<div class="chat-bubble user"> <div
<p>Hello</p> class="chat-bubble user"
:style="{ background: config.color }"
>
<p>{{ $t('INBOX_MGMT.WIDGET_BUILDER.BODY.USER_MESSAGE') }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -18,7 +33,10 @@
<div class="message-wrap"> <div class="message-wrap">
<div class="chat-bubble agent"> <div class="chat-bubble agent">
<div class="message-content"> <div class="message-content">
<p>Hello</p> <p>
{{ $t('INBOX_MGMT.WIDGET_BUILDER.BODY.AGENT_MESSAGE') }}
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -29,24 +47,74 @@
</template> </template>
<script> <script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
export default { export default {
name: 'WidgetBody', name: 'WidgetBody',
components: {
Thumbnail,
},
props: {
config: {
type: Object,
default: () => {},
},
},
computed: {
getStatusText() {
return this.config.isOnline
? this.$t('INBOX_MGMT.WIDGET_BUILDER.BODY.TEAM_AVAILABILITY.ONLINE')
: this.$t('INBOX_MGMT.WIDGET_BUILDER.BODY.TEAM_AVAILABILITY.OFFLINE');
},
getWidgetBodyClass() {
return {
'with-chat-view': !this.config.isDefaultScreen,
'with-heading-or-title':
this.config.isDefaultScreen &&
(this.config.welcomeHeading || this.config.welcomeTagline),
'with-heading-or-title-without-logo':
this.config.isDefaultScreen &&
(this.config.welcomeHeading || this.config.welcomeTagline) &&
!this.config.logo,
'without-heading-and-title':
this.config.isDefaultScreen &&
!this.config.welcomeHeading &&
!this.config.welcomeTagline,
};
},
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
$tailwind-black-700: #3c4858; .widget-body-container {
.conversation--container { .availability-content {
display: flex;
flex-direction: row;
align-items: flex-end;
padding: var(--space-one) var(--space-two) var(--space-one) var(--space-two);
min-height: inherit;
.availability-info {
width: 100%; width: 100%;
padding: var(--space-two); .team-status {
font-size: var(--font-size-default);
font-weight: var(--font-weight-medium);
}
.reply-wait-message {
font-size: var(--font-size-mini);
}
}
}
.conversation-content {
height: calc(var(--space-large) * 10);
padding: 0 var(--space-two);
.conversation-wrap { .conversation-wrap {
min-height: 200px;
.user-message { .user-message {
align-items: flex-end; align-items: flex-end;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
margin-top: 0; margin-top: var(--space-zero);
margin-bottom: var(--space-smaller); margin-bottom: var(--space-smaller);
margin-left: auto; margin-left: auto;
max-width: 85%; max-width: 85%;
@ -57,23 +125,27 @@ $tailwind-black-700: #3c4858;
max-width: 100%; max-width: 100%;
.chat-bubble { .chat-bubble {
box-shadow: var(--shadow-medium); box-shadow: var(--shadow-medium);
background: var(--color-woot); border-radius: 2rem;
border-radius: var(--border-radius-large);
color: var(--white); color: var(--white);
display: inline-block; display: inline-block;
font-size: var(--font-size-nano); font-size: var(--font-size-nano);
line-height: 1.5; line-height: 1.5;
padding: var(--space-small) var(--space-one); padding: 1.3rem 1.75rem;
text-align: left; text-align: left;
word-break: break-word;
max-width: 100%; p {
margin: var(--space-zero);
}
&.user { &.user {
border-bottom-right-radius: var(--border-radius-small); border-bottom-right-radius: var(--border-radius-small);
background: var(--color-woot);
} }
&.agent { &.agent {
background: var(--white); background: var(--white);
border-bottom-left-radius: var(--border-radius-small); border-bottom-left-radius: var(--border-radius-small);
color: $tailwind-black-700; color: var(--b-900);
}
} }
} }
} }

View file

@ -1,38 +1,102 @@
<template> <template>
<div class="footer-wrap"> <div class="footer-wrap">
<div class="input-area" /> <custom-button
v-if="config.isDefaultScreen"
class="start-conversation"
:style="{ background: config.color }"
>
{{
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.START_CONVERSATION_BUTTON_TEXT')
}}
</custom-button>
<div v-else class="chat-message-input is-focused">
<resizable-text-area
id="chat-input"
:placeholder="
$t('INBOX_MGMT.WIDGET_BUILDER.FOOTER.CHAT_INPUT_PLACEHOLDER')
"
class="user-message-input is-focused"
/>
<div class="button-wrap">
<fluent-icon icon="emoji" />
<fluent-icon class="icon-send" icon="send" />
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import CustomButton from 'dashboard/components/buttons/Button';
import ResizableTextArea from 'shared/components/ResizableTextArea';
export default { export default {
name: 'WidgetFooter', name: 'WidgetFooter',
components: {
CustomButton,
ResizableTextArea,
},
props: {
config: {
type: Object,
default: () => {},
},
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~dashboard/assets/scss/variables.scss';
.footer-wrap { .footer-wrap {
flex-shrink: 0;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
&::before { padding-left: var(--space-two);
content: ''; padding-right: var(--space-two);
position: absolute;
top: var(--space-one); .start-conversation {
left: 0; justify-content: center;
width: 100%; font-size: var(--font-size-small);
height: var(--space-one); border-radius: var(--border-radius-normal);
opacity: 0.1; padding-left: var(--space-two);
background: var(--color-background); padding-right: var(--space-two);
} }
.input-area { .chat-message-input {
background-color: var(--white); align-items: center;
display: flex;
padding: var(--space-zero) var(--space-small) var(--space-zero)
var(--space-slab);
border-radius: var(--border-radius-normal); border-radius: var(--border-radius-normal);
height: var(--space-larger); background: white;
margin: var(--space-two);
&.is-focused {
box-shadow: 0 0 0 0.1rem var(--color-woot), 0 0 0.2rem 0.2rem var(--w-100);
}
}
.button-wrap {
display: flex;
align-items: center;
padding-left: var(--space-small);
}
.user-message-input {
border: 0;
height: var(--space-medium);
min-height: var(--space-medium);
max-height: var(--space-giga);
line-height: 1;
font-size: var(--font-size-small);
resize: none;
padding: var(--space-zero);
padding-top: var(--space-smaller);
padding-bottom: var(--space-smaller);
margin-top: var(--space-small);
margin-bottom: var(--space-small);
}
.icon-send {
margin-left: var(--space-one);
} }
} }
</style> </style>

View file

@ -6,25 +6,19 @@
v-if="config.logo" v-if="config.logo"
:src="config.logo" :src="config.logo"
class="logo" class="logo"
:class="{ small: !config.isExpanded }" :class="{ small: !isDefaultScreen }"
/> />
<div v-if="!config.isExpanded"> <div v-if="!isDefaultScreen">
<div class="title-block text-base font-medium"> <div class="title-block">
<span class="mr-1">{{ config.websiteName }}</span> <span>{{ config.websiteName }}</span>
<div v-if="config.isOnline" class="online-dot" /> <div v-if="config.isOnline" class="online-dot" />
</div> </div>
<div class="text-xs mt-1 text-black-700"> <div>{{ config.replyTime }}</div>
{{ responseTime }}
</div> </div>
</div> </div>
</div> <div v-if="isDefaultScreen" class="header-expanded">
<div v-if="config.isExpanded" class="header-expanded"> <h2>{{ config.welcomeHeading }}</h2>
<h2 class="text-slate-900 mt-6 text-4xl mb-3 font-normal"> <p>{{ config.welcomeTagline }}</p>
{{ config.welcomeHeading }}
</h2>
<p class="text-lg text-black-700 leading-normal">
{{ config.welcomeTagLine }}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -35,44 +29,31 @@ export default {
props: { props: {
config: { config: {
type: Object, type: Object,
default() { default: () => {},
return {};
},
}, },
}, },
computed: { computed: {
responseTime() { isDefaultScreen() {
switch (this.config.replyTime) { return (
case 'in_a_few_minutes': this.config.isDefaultScreen &&
return this.$t( (this.config.welcomeHeading.length !== 0 ||
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_FEW_MINUTES' this.config.welcomeTagline.length !== 0)
); );
case 'in_a_few_hours':
return this.$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_FEW_HOURS'
);
case 'in_a_day':
return this.$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_DAY');
default:
return this.$t(
'INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_FEW_HOURS'
);
}
}, },
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$sucess-green: #10b981;
.header-wrapper { .header-wrapper {
flex-shrink: 0; flex-shrink: 0;
transition: max-height 300ms; transition: max-height 300ms;
background-color: var(--white); background-color: var(--white);
padding: var(--space-two); padding: var(--space-two);
border-top-left-radius: var(--border-radius-large);
border-top-right-radius: var(--border-radius-large);
.header-branding { .header-branding {
max-height: 16rem;
.header { .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -90,22 +71,34 @@ $sucess-green: #10b981;
} }
} }
} }
.header-expanded {
max-height: var(--space-giga);
overflow: scroll;
h2 {
font-size: var(--font-size-big);
margin-bottom: var(--space-small);
margin-top: var(--space-two);
overflow-wrap: break-word;
} }
.text-base {
font-size: var(--font-size-default); p {
font-size: var(--font-size-small);
overflow-wrap: break-word;
} }
.mt-6 {
margin-top: var(--space-medium);
} }
}
.title-block { .title-block {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: var(--font-size-default);
.online-dot { .online-dot {
background-color: $sucess-green; background-color: var(--g-500);
height: var(--space-small); height: var(--space-small);
width: var(--space-small); width: var(--space-small);
border-radius: 100%; border-radius: 100%;
margin: var(--space-zero) var(--space-one); margin: var(--space-zero) var(--space-smaller);
} }
} }
} }

View file

@ -325,6 +325,9 @@
<div v-if="selectedTabKey === 'businesshours'"> <div v-if="selectedTabKey === 'businesshours'">
<weekly-availability :inbox="inbox" /> <weekly-availability :inbox="inbox" />
</div> </div>
<div v-if="selectedTabKey === 'widgetBuilder'">
<widget-builder :inbox="inbox" />
</div>
</div> </div>
</template> </template>
@ -342,6 +345,7 @@ import WeeklyAvailability from './components/WeeklyAvailability';
import GreetingsEditor from 'shared/components/GreetingsEditor'; import GreetingsEditor from 'shared/components/GreetingsEditor';
import ConfigurationPage from './settingsPage/ConfigurationPage'; import ConfigurationPage from './settingsPage/ConfigurationPage';
import CollaboratorsPage from './settingsPage/CollaboratorsPage'; import CollaboratorsPage from './settingsPage/CollaboratorsPage';
import WidgetBuilder from './WidgetBuilder';
export default { export default {
components: { components: {
@ -353,6 +357,7 @@ export default {
GreetingsEditor, GreetingsEditor,
ConfigurationPage, ConfigurationPage,
CollaboratorsPage, CollaboratorsPage,
WidgetBuilder,
}, },
mixins: [alertMixin, configMixin, inboxMixin], mixins: [alertMixin, configMixin, inboxMixin],
data() { data() {
@ -410,6 +415,10 @@ export default {
key: 'configuration', key: 'configuration',
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'), name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
}, },
{
key: 'widgetBuilder',
name: this.$t('INBOX_MGMT.TABS.WIDGET_BUILDER'),
},
]; ];
} }

View file

@ -0,0 +1,471 @@
<template>
<div class="settings--content">
<div class="widget-builder-conatiner">
<div class="settings-container">
<div class="settings-content">
<form @submit.prevent="updateWidget">
<woot-avatar-uploader
:label="
$t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.AVATAR.LABEL')
"
:src="avatarUrl"
delete-avatar
@change="handleImageUpload"
@onAvatarDelete="handleAvatarDelete"
/>
<woot-input
v-model.trim="websiteName"
:class="{ error: $v.websiteName.$error }"
:label="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WEBSITE_NAME.LABEL'
)
"
:placeholder="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WEBSITE_NAME.PLACE_HOLDER'
)
"
:error="websiteNameValidationErrorMsg"
@blur="$v.websiteName.$touch"
/>
<woot-input
v-model.trim="welcomeHeading"
:label="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_HEADING.LABEL'
)
"
:placeholder="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_HEADING.PLACE_HOLDER'
)
"
/>
<woot-input
v-model.trim="welcomeTagline"
:label="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_TAGLINE.LABEL'
)
"
:placeholder="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WELCOME_TAGLINE.PLACE_HOLDER'
)
"
/>
<label>
{{
$t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.REPLY_TIME.LABEL')
}}
<select v-model="replyTime">
<option
v-for="option in getReplyTimeOptions"
:key="option.key"
:value="option.value"
>
{{ option.text }}
</option>
</select>
</label>
<label>
{{
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_COLOR_LABEL'
)
}}
<woot-color-picker v-model="color" />
</label>
<input-radio-group
name="widget-bubble-position"
:label="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_POSITION_LABEL'
)
"
:items="widgetBubblePositions"
:action="handleWidgetBubblePositionChange"
/>
<input-radio-group
name="widget-bubble-type"
:label="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_TYPE_LABEL'
)
"
:items="widgetBubbleTypes"
:action="handleWidgetBubbleTypeChange"
/>
<woot-input
v-model.trim="widgetBubbleLauncherTitle"
:label="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_LAUNCHER_TITLE.LABEL'
)
"
:placeholder="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_LAUNCHER_TITLE.PLACE_HOLDER'
)
"
/>
<woot-submit-button
class="submit-button"
:button-text="
$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.UPDATE.BUTTON_TEXT'
)
"
:loading="uiFlags.isUpdating"
:disabled="$v.$invalid || uiFlags.isUpdating"
/>
</form>
</div>
</div>
<div class="widget-container">
<input-radio-group
name="widget-view-options"
:items="getWidgetViewOptions"
:action="handleWidgetViewChange"
:style="{ 'text-align': 'center' }"
/>
<div v-if="isWidgetPreview" class="widget-preview">
<Widget
:welcome-heading="welcomeHeading"
:welcome-tagline="welcomeTagline"
:website-name="websiteName"
:logo="avatarUrl"
is-online
:reply-time="replyTime"
:color="color"
:widget-bubble-position="widgetBubblePosition"
:widget-bubble-launcher-title="widgetBubbleLauncherTitle"
:widget-bubble-type="widgetBubbleType"
/>
</div>
<div v-else class="widget-script">
<woot-code :script="widgetScript" />
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import Widget from 'dashboard/modules/widget-preview/components/Widget';
import InputRadioGroup from './components/InputRadioGroup';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import LocalStorage from 'dashboard/helper/localStorage';
export default {
components: {
Widget,
InputRadioGroup,
},
mixins: [alertMixin],
props: {
inbox: {
type: Object,
default: () => {},
},
},
data() {
return {
isWidgetPreview: true,
color: '#1f93ff',
websiteName: '',
welcomeHeading: '',
welcomeTagline: '',
replyTime: 'in_a_few_minutes',
avatarFile: null,
avatarUrl: '',
widgetBubblePosition: 'right',
widgetBubbleLauncherTitle: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_LAUNCHER_TITLE.DEFAULT'
),
widgetBubbleType: 'standard',
widgetBubblePositions: [
{
id: 'left',
title: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_POSITION.LEFT'
),
checked: false,
},
{
id: 'right',
title: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_POSITION.RIGHT'
),
checked: true,
},
],
widgetBubbleTypes: [
{
id: 'standard',
title: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_TYPE.STANDARD'
),
checked: true,
},
{
id: 'expanded_bubble',
title: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_BUBBLE_TYPE.EXPANDED_BUBBLE'
),
checked: false,
},
],
};
},
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
}),
widgetScript() {
let options = {
position: this.widgetBubblePosition,
type: this.widgetBubbleType,
launcherTitle: this.widgetBubbleLauncherTitle,
};
let script = this.inbox.web_widget_script;
return (
script.substring(0, 13) +
this.$t('INBOX_MGMT.WIDGET_BUILDER.SCRIPT_SETTINGS', {
options: JSON.stringify(options),
}) +
script.substring(13, script.length)
);
},
getWidgetViewOptions() {
return [
{
id: 'preview',
title: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_VIEW_OPTION.PREVIEW'
),
checked: true,
},
{
id: 'script',
title: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WIDGET_VIEW_OPTION.SCRIPT'
),
checked: false,
},
];
},
getReplyTimeOptions() {
return [
{
key: 'in_a_few_minutes',
value: 'in_a_few_minutes',
text: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.REPLY_TIME.IN_A_FEW_MINUTES'
),
},
{
key: 'in_a_few_hours',
value: 'in_a_few_hours',
text: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.REPLY_TIME.IN_A_FEW_HOURS'
),
},
{
key: 'in_a_day',
value: 'in_a_day',
text: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.REPLY_TIME.IN_A_DAY'
),
},
];
},
websiteNameValidationErrorMsg() {
return this.$v.websiteName.$error
? this.$t('INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.WEBSITE_NAME.ERROR')
: '';
},
},
mounted() {
this.setDefaults();
},
validations: {
websiteName: { required },
},
methods: {
setDefaults() {
// Widget Settings
const {
name,
welcome_title,
welcome_tagline,
widget_color,
reply_time,
avatar_url,
} = this.inbox;
this.websiteName = name;
this.welcomeHeading = welcome_title;
this.welcomeTagline = welcome_tagline;
this.color = widget_color;
this.replyTime = reply_time;
this.avatarUrl = avatar_url;
// Widget Bubble Settings
const { key, storage } = this.getLocalStorageWithKey(this.inbox.id);
this.widgetBubblePositions = this.widgetBubblePositions.map(item => {
if (item.id === storage.get(key).position) {
item.checked = true;
this.widgetBubblePosition = item.id;
}
return item;
});
this.widgetBubbleTypes = this.widgetBubbleTypes.map(item => {
if (item.id === storage.get(key).type) {
item.checked = true;
this.widgetBubbleType = item.id;
}
return item;
});
this.widgetBubbleLauncherTitle =
storage.get(key).launcherTitle || 'Chat with us';
},
handleWidgetBubblePositionChange(item) {
this.widgetBubblePosition = item.id;
},
handleWidgetBubbleTypeChange(item) {
this.widgetBubbleType = item.id;
},
handleWidgetViewChange(item) {
this.isWidgetPreview = item.id === 'preview';
},
handleImageUpload({ file, url }) {
this.avatarFile = file;
this.avatarUrl = url;
},
async handleAvatarDelete() {
try {
await this.$store.dispatch('inboxes/deleteInboxAvatar', this.inbox.id);
this.avatarFile = null;
this.avatarUrl = '';
this.showAlert(
this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.AVATAR.DELETE.API.SUCCESS_MESSAGE'
)
);
} catch (error) {
this.showAlert(
error.message
? error.message
: this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.AVATAR.DELETE.API.ERROR_MESSAGE'
)
);
}
},
async updateWidget() {
const bubbleSettings = {
position: this.widgetBubblePosition,
launcherTitle: this.widgetBubbleLauncherTitle,
type: this.widgetBubbleType,
};
this.getLocalStorageWithKey(this.inbox.id).storage.store(bubbleSettings);
try {
const payload = {
id: this.inbox.id,
name: this.websiteName,
channel: {
widget_color: this.color,
welcome_title: this.welcomeHeading,
welcome_tagline: this.welcomeTagline,
reply_time: this.replyTime,
},
};
if (this.avatarFile) {
payload.avatar = this.avatarFile;
}
await this.$store.dispatch('inboxes/updateInbox', payload);
this.showAlert(
this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.UPDATE.API.SUCCESS_MESSAGE'
)
);
} catch (error) {
this.showAlert(
error.message ||
this.$t(
'INBOX_MGMT.WIDGET_BUILDER.WIDGET_OPTIONS.UPDATE.API.ERROR_MESSAGE'
)
);
}
},
getLocalStorageWithKey(id) {
const storageKey = `widgetBubble_${id}`;
return {
key: storageKey,
storage: new LocalStorage(storageKey),
};
},
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/woot';
.widget-builder-conatiner {
display: flex;
flex-direction: row;
padding: var(--space-one);
@include breakpoint(900px down) {
flex-direction: column;
}
}
.settings-container {
width: 40%;
@include breakpoint(900px down) {
width: 100%;
}
.settings-content {
padding: var(--space-normal) var(--space-zero);
overflow-y: scroll;
min-height: 100%;
.submit-button {
margin-top: var(--space-normal);
}
}
}
.widget-container {
width: 60%;
@include breakpoint(900px down) {
width: 100%;
}
.widget-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
min-height: 64rem;
margin: var(--space-zero) var(--space-two) var(--space-two) var(--space-two);
padding: var(--space-one) var(--space-one) var(--space-one) var(--space-one);
background: var(--s-50);
@include breakpoint(500px down) {
background: none;
}
}
.widget-script {
margin: 0 var(--space-two);
padding: var(--space-one);
background: var(--s-50);
}
}
</style>

View file

@ -2,14 +2,17 @@
<div> <div>
<label class="radio-group-label">{{ label }}</label> <label class="radio-group-label">{{ label }}</label>
<div class="radio-group"> <div class="radio-group">
<div v-for="item in items" :key="item.id"> <div v-for="item in items" :key="item.id" class="radio-group-item">
<label class="radio-group-item-label">
<input <input
name="radio-input" class="radio-input"
:name="`${name} -radio-input`"
type="radio" type="radio"
:checked="item.checked" :checked="item.checked"
@change="action(item)" @change="action({ ...item, checked: true })"
/> />
<label>{{ item.title }}</label> <span>{{ item.title }}</span>
</label>
</div> </div>
</div> </div>
</div> </div>
@ -18,6 +21,10 @@
<script> <script>
export default { export default {
props: { props: {
name: {
type: String,
default: 'string',
},
label: { label: {
type: String, type: String,
default: '', default: '',
@ -36,10 +43,23 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.radio-group-label { .radio-group-label {
margin-bottom: var(--space-small); margin-bottom: var(--space-smaller);
} }
.radio-group { .radio-group {
display: inline-block;
margin-bottom: var(--space-small);
}
.radio-group-item {
float: left;
margin-right: var(--space-one);
.radio-group-item-label {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer;
.radio-input {
margin: 0 var(--space-one) 0 0;
}
}
} }
</style> </style>

View file

@ -15,4 +15,5 @@
6px 3px 22px 9px rgb(181 181 181 / 25%); 6px 3px 22px 9px rgb(181 181 181 / 25%);
--shadow-context-menu: rgb(22 23 24 / 35%) 0px 10px 38px -10px, --shadow-context-menu: rgb(22 23 24 / 35%) 0px 10px 38px -10px,
rgb(22 23 24 / 20%) 0px 10px 20px -15px; rgb(22 23 24 / 20%) 0px 10px 20px -15px;
--shadow-widget-builder: 0 0px 20px 5px rgb(0 0 0 / 10%);
} }