@@ -1,3 +1,4 @@
|
||||
|
||||
.button {
|
||||
font-family: $body-font-family;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
@@ -14,5 +14,4 @@
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
@import 'woot';
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getContrastingTextColor } from 'shared/helpers/ColorHelper';
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
@@ -43,12 +44,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
textColor() {
|
||||
const color = this.bgColor.replace('#', '');
|
||||
const r = parseInt(color.slice(0, 2), 16);
|
||||
const g = parseInt(color.slice(2, 4), 16);
|
||||
const b = parseInt(color.slice(4, 6), 16);
|
||||
// http://stackoverflow.com/a/3943023/112731
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF';
|
||||
return getContrastingTextColor(this.bgColor);
|
||||
},
|
||||
labelClass() {
|
||||
return `label ${this.small ? 'small' : ''}`;
|
||||
|
||||
@@ -73,6 +73,13 @@
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"TITLE": "Set Reply time",
|
||||
"IN_A_FEW_MINUTES": "In a few minutes",
|
||||
"IN_A_FEW_HOURS": "In a few hours",
|
||||
"IN_A_DAY": "In a day",
|
||||
"HELP_TEXT": "This reply time will be displayed on the live chat widget"
|
||||
},
|
||||
"WIDGET_COLOR": {
|
||||
"LABEL": "Widget Color",
|
||||
"PLACEHOLDER": "Update the widget color used in widget"
|
||||
|
||||
@@ -78,8 +78,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import Auth from '../../api/auth';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="medium-12 columns text-center">
|
||||
<div class="website--code">
|
||||
<woot-code
|
||||
v-if="currentInbox.website_token"
|
||||
v-if="currentInbox.web_widget_script"
|
||||
:script="currentInbox.web_widget_script"
|
||||
>
|
||||
</woot-code>
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||
}
|
||||
|
||||
if (!this.currentInbox.website_token) {
|
||||
if (!this.currentInbox.web_widget_script) {
|
||||
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
|
||||
}
|
||||
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
}}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<woot-input
|
||||
v-if="greetingEnabled"
|
||||
v-model.trim="greetingMessage"
|
||||
@@ -116,6 +117,30 @@
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<label class="medium-9 columns">
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.TITLE') }}
|
||||
<select v-model="replyTime">
|
||||
<option key="in_a_few_minutes" value="in_a_few_minutes">
|
||||
{{
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_FEW_MINUTES')
|
||||
}}
|
||||
</option>
|
||||
<option key="in_a_few_hours" value="in_a_few_hours">
|
||||
{{
|
||||
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_FEW_HOURS')
|
||||
}}
|
||||
</option>
|
||||
<option key="in_a_day" value="in_a_day">
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.IN_A_DAY') }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<p class="help-text">
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.REPLY_TIME.HELP_TEXT') }}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label class="medium-9 columns">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT') }}
|
||||
<select v-model="autoAssignment">
|
||||
@@ -220,7 +245,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
import { mapGetters } from 'vuex';
|
||||
import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
@@ -249,6 +273,7 @@ export default {
|
||||
channelWelcomeTitle: '',
|
||||
channelWelcomeTagline: '',
|
||||
selectedFeatureFlags: [],
|
||||
replyTime: '',
|
||||
autoAssignmentOptions: [
|
||||
{
|
||||
value: true,
|
||||
@@ -352,6 +377,7 @@ export default {
|
||||
this.channelWelcomeTitle = this.inbox.welcome_title;
|
||||
this.channelWelcomeTagline = this.inbox.welcome_tagline;
|
||||
this.selectedFeatureFlags = this.inbox.selected_feature_flags || [];
|
||||
this.replyTime = this.inbox.reply_time;
|
||||
});
|
||||
},
|
||||
async fetchAttachedAgents() {
|
||||
@@ -364,7 +390,7 @@ export default {
|
||||
} = response;
|
||||
this.selectedAgents = inboxMembers;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// Handle error
|
||||
}
|
||||
},
|
||||
async updateAgents() {
|
||||
@@ -395,6 +421,7 @@ export default {
|
||||
welcome_title: this.channelWelcomeTitle || '',
|
||||
welcome_tagline: this.channelWelcomeTagline || '',
|
||||
selectedFeatureFlags: this.selectedFeatureFlags,
|
||||
reply_time: this.replyTime || 'in_a_few_minutes',
|
||||
},
|
||||
};
|
||||
if (this.avatarFile) {
|
||||
@@ -409,7 +436,6 @@ export default {
|
||||
handleImageUpload({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = url;
|
||||
console.log(this.avatarUrl);
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
|
||||
@@ -6,3 +6,12 @@
|
||||
src: url('~shared/assets/fonts/Inter-Regular.woff2?v=3.11') format('woff2'),
|
||||
url('~shared/assets/fonts/Inter-Regular.woff?v=3.11') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('~shared/assets/fonts/Inter-Medium.woff2?v=3.11') format('woff2'),
|
||||
url('~shared/assets/fonts/Inter-Medium.woff?v=3.11') format('woff');
|
||||
}
|
||||
|
||||
54
app/javascript/shared/components/Button.vue
Normal file
54
app/javascript/shared/components/Button.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<button :class="buttonClassName" :style="buttonStyles" @click="onClick">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'blue',
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
buttonClassName() {
|
||||
let className = 'text-white py-3 px-4 rounded shadow-sm';
|
||||
if (this.type === 'blue' && !Object.keys(this.buttonStyles).length) {
|
||||
className = `${className} bg-woot-500 hover:bg-woot-700`;
|
||||
}
|
||||
if (this.block) {
|
||||
className = `${className} w-full`;
|
||||
}
|
||||
return className;
|
||||
},
|
||||
buttonStyles() {
|
||||
const styles = {};
|
||||
if (this.bgColor) {
|
||||
styles.backgroundColor = this.bgColor;
|
||||
}
|
||||
if (this.textColor) {
|
||||
styles.color = this.textColor;
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
},
|
||||
minHeight: {
|
||||
type: Number,
|
||||
default: 3.2,
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
8
app/javascript/shared/helpers/ColorHelper.js
Normal file
8
app/javascript/shared/helpers/ColorHelper.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const getContrastingTextColor = bgColor => {
|
||||
const color = bgColor.replace('#', '');
|
||||
const r = parseInt(color.slice(0, 2), 16);
|
||||
const g = parseInt(color.slice(2, 4), 16);
|
||||
const b = parseInt(color.slice(4, 6), 16);
|
||||
// http://stackoverflow.com/a/3943023/112731
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF';
|
||||
};
|
||||
@@ -1,31 +1,30 @@
|
||||
// Font sizes
|
||||
$font-size-nano: 0.8rem;
|
||||
$font-size-micro: 0.8rem;
|
||||
$font-size-mini: 1rem;
|
||||
$font-size-small: 1.2rem;
|
||||
$font-size-default: 1.4rem;
|
||||
$font-size-medium: 1.6rem;
|
||||
$font-size-large: 2rem;
|
||||
$font-size-big: 2.4rem;
|
||||
$font-size-bigger: 3.2rem;
|
||||
$font-size-mega: 4rem;
|
||||
$font-size-giga: 5.6rem;
|
||||
$font-size-micro: 0.5rem;
|
||||
$font-size-mini: 0.625rem;
|
||||
$font-size-small: 0.75rem;
|
||||
$font-size-default: 0.875rem;
|
||||
$font-size-medium: 1rem;
|
||||
$font-size-large: 1.25rem;
|
||||
$font-size-big: 1.5rem;
|
||||
$font-size-bigger: 2rem;
|
||||
$font-size-mega: 2.5rem;
|
||||
$font-size-giga: 3.5rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.2rem;
|
||||
$space-smaller: 0.4rem;
|
||||
$space-small: 0.8rem;
|
||||
$space-one: 1rem;
|
||||
$space-slab: 1.2rem;
|
||||
$space-normal: 1.6rem;
|
||||
$space-two: 2rem;
|
||||
$space-medium: 2.4rem;
|
||||
$space-large: 3.2rem;
|
||||
$space-larger: 4.8rem;
|
||||
$space-big: 6.4rem;
|
||||
$space-jumbo: 8rem;
|
||||
$space-mega: 10rem;
|
||||
$space-micro: 0.125rem;
|
||||
$space-smaller: 0.25rem;
|
||||
$space-small: 0.5rem;
|
||||
$space-one: 0.625rem;
|
||||
$space-slab: 0.75rem;
|
||||
$space-normal: 1rem;
|
||||
$space-two: 1.25rem;
|
||||
$space-medium: 1.5rem;
|
||||
$space-large: 2rem;
|
||||
$space-larger: 3rem;
|
||||
$space-big: 4rem;
|
||||
$space-jumbo: 5rem;
|
||||
$space-mega: 6.25rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
@@ -35,15 +34,6 @@ $font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 700;
|
||||
|
||||
//Navbar
|
||||
$nav-bar-width: 23rem;
|
||||
$header-height: 5.6rem;
|
||||
|
||||
// Woot Logo
|
||||
$woot-logo-width: 20rem;
|
||||
$woot-logo-height: 8rem;
|
||||
$woot-logo-padding: $space-large $space-large $space-large $space-large;
|
||||
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-primary: $color-woot;
|
||||
@@ -65,20 +55,6 @@ $color-error: #ff382d;
|
||||
$color-primary-light: #c7e3ff;
|
||||
$color-primary-dark: darken($color-woot, 20%);
|
||||
|
||||
|
||||
// Thumbnail
|
||||
$thumbnail-radius: 4rem;
|
||||
|
||||
// chat-header
|
||||
$conv-header-height: 4rem;
|
||||
|
||||
// login
|
||||
|
||||
// Inbox List
|
||||
|
||||
$inbox-thumb-size: 4.8rem;
|
||||
|
||||
|
||||
// Spinner
|
||||
$spinkit-spinner-color: $color-white !default;
|
||||
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
|
||||
@@ -92,7 +68,7 @@ $swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
$border-radius: 3px;
|
||||
$border-radius: 0.1875px;
|
||||
$line-height: 1;
|
||||
$footer-height: 11.2rem;
|
||||
$header-expanded-height: $space-medium * 10;
|
||||
@@ -109,10 +85,6 @@ Arial,
|
||||
sans-serif;
|
||||
$ionicons-font-path: '~ionicons/fonts';
|
||||
|
||||
$spinkit-spinner-color: $color-white !default;
|
||||
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
|
||||
$spinkit-size: 1.6rem !default;
|
||||
|
||||
// Break points
|
||||
$break-point-medium: 667px;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
@import 'reset';
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'variables';
|
||||
@import 'buttons';
|
||||
@import 'mixins';
|
||||
@@ -10,7 +13,6 @@
|
||||
html,
|
||||
body {
|
||||
font-family: $font-family;
|
||||
font-size: 10px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
|
||||
@@ -25,8 +25,8 @@ export default {
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.typing-bubble {
|
||||
max-width: $space-medium;
|
||||
padding: $space-smaller $space-small;
|
||||
max-width: $space-normal * 2.4;
|
||||
padding: $space-small;
|
||||
border-bottom-left-radius: $space-two;
|
||||
border-top-left-radius: $space-small;
|
||||
|
||||
|
||||
@@ -1,80 +1,27 @@
|
||||
<template>
|
||||
<div class="available-agents">
|
||||
<div class="toast-bg">
|
||||
<div class="avatars-wrap">
|
||||
<GroupedAvatars :users="users" />
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<grouped-avatars :users="users" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GroupedAvatars from 'widget/components/GroupedAvatars.vue';
|
||||
import agentMixin from '../mixins/agentMixin';
|
||||
|
||||
export default {
|
||||
name: 'AvailableAgents',
|
||||
components: { GroupedAvatars },
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
agents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
return this.agents.map(agent => ({
|
||||
return this.agents.slice(0, 5).map(agent => ({
|
||||
id: agent.id,
|
||||
avatar: agent.avatar_url,
|
||||
name: agent.name,
|
||||
}));
|
||||
},
|
||||
title() {
|
||||
return this.getAvailableAgentsText(this.agents);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
@import '~widget/assets/scss/mixins.scss';
|
||||
|
||||
.available-agents {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
margin: $space-normal $space-medium;
|
||||
box-sizing: border-box;
|
||||
|
||||
.toast-bg {
|
||||
border-radius: $space-large;
|
||||
background: $color-body;
|
||||
@include shadow-medium;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $color-white;
|
||||
padding: $space-small $space-normal $space-small $space-small;
|
||||
line-height: 1.4;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.avatars-wrap {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: $space-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
<template>
|
||||
<header class="header-collapsed">
|
||||
<div class="header-branding">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" alt="avatar" />
|
||||
<h2 class="title" v-html="title"></h2>
|
||||
<img
|
||||
v-if="avatarUrl"
|
||||
class="inbox--avatar mr-3"
|
||||
:src="avatarUrl"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-black-900 font-medium text-base flex items-center">
|
||||
<span class="mr-1" v-html="title" />
|
||||
<div
|
||||
:class="
|
||||
`status-view--badge rounded-full leading-4 ${
|
||||
availableAgents.length ? 'bg-green-500' : 'hidden'
|
||||
}`
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs mt-1 text-black-700">
|
||||
{{ replyTimeStatus }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<header-actions :show-popout-button="showPopoutButton" />
|
||||
</header>
|
||||
@@ -11,11 +30,15 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import HeaderActions from './HeaderActions';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import teamAvailabilityMixin from 'widget/mixins/teamAvailabilityMixin';
|
||||
|
||||
export default {
|
||||
name: 'ChatHeader',
|
||||
components: {
|
||||
HeaderActions,
|
||||
},
|
||||
mixins: [configMixin, teamAvailabilityMixin],
|
||||
props: {
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
@@ -29,6 +52,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -48,7 +75,8 @@ export default {
|
||||
padding: $space-two $space-medium;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
color: $color-white;
|
||||
background: white;
|
||||
@include shadow-large;
|
||||
|
||||
.header-branding {
|
||||
display: flex;
|
||||
@@ -60,15 +88,17 @@ export default {
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-size-large;
|
||||
font-weight: $font-weight-medium;
|
||||
color: $color-heading;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: $space-small;
|
||||
.inbox--avatar {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-view--badge {
|
||||
height: $space-small;
|
||||
width: $space-small;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<header class="header-expanded">
|
||||
<div class="header--row">
|
||||
<header class="header-expanded py-8 px-6 bg-white relative box-border w-full">
|
||||
<div class="flex justify-between items-start">
|
||||
<img v-if="avatarUrl" class="logo" :src="avatarUrl" />
|
||||
<header-actions :show-popout-button="showPopoutButton" />
|
||||
</div>
|
||||
<h2 class="title" v-html="introHeading"></h2>
|
||||
<p class="body" v-html="introBody"></p>
|
||||
<h2
|
||||
class="text-slate-900 mt-6 text-4xl mb-3 font-normal"
|
||||
v-html="introHeading"
|
||||
/>
|
||||
<p class="text-lg text-black-700 leading-normal" v-html="introBody" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -44,38 +47,14 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
@import '~widget/assets/scss/mixins.scss';
|
||||
|
||||
.header-expanded {
|
||||
padding: $space-large $space-medium $space-large;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
@include shadow-large;
|
||||
|
||||
.logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $color-heading;
|
||||
font-size: $font-size-mega;
|
||||
font-weight: $font-weight-normal;
|
||||
margin-bottom: $space-slab;
|
||||
margin-top: $space-medium;
|
||||
}
|
||||
|
||||
.body {
|
||||
color: $color-body;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.header--row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="disabled"
|
||||
class="send-button"
|
||||
class="send-button ml-1"
|
||||
@click="onClick"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<div class="avatars">
|
||||
<span v-for="user in users" :key="user.id" class="avatar">
|
||||
<Thumbnail
|
||||
size="24px"
|
||||
<div class="flex overflow-hidden">
|
||||
<span
|
||||
v-for="(user, index) in users"
|
||||
:key="user.id"
|
||||
:class="
|
||||
`${
|
||||
index ? '-ml-4' : ''
|
||||
} inline-block rounded-full text-white shadow-solid`
|
||||
"
|
||||
>
|
||||
<thumbnail
|
||||
size="40px"
|
||||
:username="user.name"
|
||||
:src="user.avatar"
|
||||
has-border
|
||||
@@ -25,22 +33,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
@import '~widget/assets/scss/mixins.scss';
|
||||
|
||||
.avatars {
|
||||
display: inline-block;
|
||||
padding-left: $space-one;
|
||||
|
||||
.avatar {
|
||||
margin-left: -$space-slab;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: $space-medium;
|
||||
height: $space-medium;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="isIframe" class="actions">
|
||||
<div v-if="isIframe" class="actions flex items-center">
|
||||
<button
|
||||
v-if="showPopoutButton"
|
||||
class="button transparent compact new-window--button"
|
||||
@@ -66,9 +66,6 @@ export default {
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
margin-left: $space-normal;
|
||||
}
|
||||
|
||||
59
app/javascript/widget/components/TeamAvailability.vue
Normal file
59
app/javascript/widget/components/TeamAvailability.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="px-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-black-700">
|
||||
<div class="text-base leading-5 font-medium mb-1">
|
||||
{{ teamAvailabilityStatus }}
|
||||
</div>
|
||||
<div class="text-xs leading-4 mt-1">
|
||||
{{ replyTimeStatus }}
|
||||
</div>
|
||||
</div>
|
||||
<available-agents :agents="availableAgents" />
|
||||
</div>
|
||||
<woot-button
|
||||
class="font-medium"
|
||||
block
|
||||
:bg-color="widgetColor"
|
||||
:text-color="textColor"
|
||||
@click="startConversation"
|
||||
>
|
||||
{{ $t('START_CONVERSATION') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||
import { getContrastingTextColor } from 'shared/helpers/ColorHelper';
|
||||
import WootButton from 'shared/components/Button';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
import teamAvailabilityMixin from 'widget/mixins/teamAvailabilityMixin';
|
||||
|
||||
export default {
|
||||
name: 'TeamAvailability',
|
||||
components: {
|
||||
AvailableAgents,
|
||||
WootButton,
|
||||
},
|
||||
mixins: [configMixin, teamAvailabilityMixin],
|
||||
props: {
|
||||
availableAgents: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startConversation() {
|
||||
this.$emit('start-conversation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -8,12 +8,16 @@
|
||||
"SUBMIT": "Submit"
|
||||
}
|
||||
},
|
||||
"AGENT_AVAILABILITY": {
|
||||
"IS_AVAILABLE": "is available",
|
||||
"ARE_AVAILABLE": "are available",
|
||||
"OTHERS_ARE_AVAILABLE": "others are available",
|
||||
"AND": "and"
|
||||
"TEAM_AVAILABILITY": {
|
||||
"ONLINE": "We are online",
|
||||
"OFFLINE": "We are offline"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"START_CONVERSATION": "Start Conversation",
|
||||
"UNREAD_VIEW": {
|
||||
"VIEW_MESSAGES_BUTTON": "See new messages",
|
||||
"CLOSE_MESSAGES_BUTTON": "Close"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export default {
|
||||
methods: {
|
||||
getAvailableAgentsText(agents) {
|
||||
const count = agents.length;
|
||||
if (count === 1) {
|
||||
const [agent] = agents;
|
||||
return `${agent.name} ${this.$t('AGENT_AVAILABILITY.IS_AVAILABLE')}`;
|
||||
}
|
||||
|
||||
if (count === 2) {
|
||||
const [first, second] = agents;
|
||||
return `${first.name} ${this.$t('AGENT_AVAILABILITY.AND')} ${
|
||||
second.name
|
||||
} ${this.$t('AGENT_AVAILABILITY.ARE_AVAILABLE')}`;
|
||||
}
|
||||
|
||||
const [agent] = agents;
|
||||
const rest = agents.length - 1;
|
||||
return `${agent.name} ${this.$t(
|
||||
'AGENT_AVAILABILITY.AND'
|
||||
)} ${rest} ${this.$t('AGENT_AVAILABILITY.OTHERS_ARE_AVAILABLE')}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -21,5 +21,20 @@ export default {
|
||||
hasAttachmentsEnabled() {
|
||||
return this.channelConfig.enabledFeatures.includes('attachments');
|
||||
},
|
||||
replyTime() {
|
||||
return window.chatwootWebChannel.replyTime;
|
||||
},
|
||||
replyTimeStatus() {
|
||||
switch (this.replyTime) {
|
||||
case 'in_a_few_minutes':
|
||||
return this.$t('REPLY_TIME.IN_A_FEW_MINUTES');
|
||||
case 'in_a_few_hours':
|
||||
return this.$t('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
case 'in_a_day':
|
||||
return this.$t('REPLY_TIME.IN_A_DAY');
|
||||
default:
|
||||
return this.$t('REPLY_TIME.IN_A_FEW_HOURS');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import agentMixin from '../agentMixin';
|
||||
import Vue from 'vue';
|
||||
|
||||
const translations = {
|
||||
'AGENT_AVAILABILITY.IS_AVAILABLE': 'is available',
|
||||
'AGENT_AVAILABILITY.ARE_AVAILABLE': 'are available',
|
||||
'AGENT_AVAILABILITY.OTHERS_ARE_AVAILABLE': 'others are available',
|
||||
'AGENT_AVAILABILITY.AND': 'and',
|
||||
};
|
||||
|
||||
const TestComponent = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [agentMixin],
|
||||
methods: {
|
||||
$t(key) {
|
||||
return translations[key];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('agentMixin', () => {
|
||||
test('returns correct text', () => {
|
||||
const Constructor = Vue.extend(TestComponent);
|
||||
const vm = new Constructor().$mount();
|
||||
const wrapper = createWrapper(vm);
|
||||
|
||||
expect(wrapper.vm.getAvailableAgentsText([{ name: 'Pranav' }])).toEqual(
|
||||
'Pranav is available'
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.vm.getAvailableAgentsText([
|
||||
{ name: 'Pranav' },
|
||||
{ name: 'Nithin' },
|
||||
])
|
||||
).toEqual('Pranav and Nithin are available');
|
||||
|
||||
expect(
|
||||
wrapper.vm.getAvailableAgentsText([
|
||||
{ name: 'Pranav' },
|
||||
{ name: 'Nithin' },
|
||||
{ name: 'Subin' },
|
||||
{ name: 'Sojan' },
|
||||
])
|
||||
).toEqual('Pranav and 3 others are available');
|
||||
});
|
||||
});
|
||||
10
app/javascript/widget/mixins/teamAvailabilityMixin.js
Normal file
10
app/javascript/widget/mixins/teamAvailabilityMixin.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
computed: {
|
||||
teamAvailabilityStatus() {
|
||||
if (this.availableAgents.length) {
|
||||
return this.$t('TEAM_AVAILABILITY.ONLINE');
|
||||
}
|
||||
return this.$t('TEAM_AVAILABILITY.OFFLINE');
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,26 +1,55 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div
|
||||
v-if="!conversationSize && isFetchingList"
|
||||
class="flex flex-1 items-center h-full bg-black-25 justify-center"
|
||||
>
|
||||
<spinner size=""></spinner>
|
||||
</div>
|
||||
<div v-else class="home">
|
||||
<div class="header-wrap">
|
||||
<ChatHeaderExpanded
|
||||
v-if="isHeaderExpanded && !hideWelcomeHeader"
|
||||
:intro-heading="introHeading"
|
||||
:intro-body="introBody"
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
:show-popout-button="showPopoutButton"
|
||||
/>
|
||||
<ChatHeader
|
||||
v-else
|
||||
:title="channelConfig.websiteName"
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
:show-popout-button="showPopoutButton"
|
||||
/>
|
||||
<transition
|
||||
enter-active-class="transition-all delay-200 duration-300 ease"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
enter-class="opacity-0 transform -translate-y-32"
|
||||
enter-to-class="opacity-100 transform translate-y-0"
|
||||
leave-class="opacity-100 transform translate-y-0"
|
||||
leave-to-class="opacity-0 transform -translate-y-32"
|
||||
>
|
||||
<chat-header-expanded
|
||||
v-if="!isOnMessageView"
|
||||
:intro-heading="introHeading"
|
||||
:intro-body="introBody"
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
:show-popout-button="showPopoutButton"
|
||||
/>
|
||||
<chat-header
|
||||
v-if="isOnMessageView"
|
||||
:title="channelConfig.websiteName"
|
||||
:avatar-url="channelConfig.avatarUrl"
|
||||
:show-popout-button="showPopoutButton"
|
||||
:available-agents="availableAgents"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
<AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" />
|
||||
<ConversationWrap :grouped-messages="groupedMessages" />
|
||||
<conversation-wrap :grouped-messages="groupedMessages" />
|
||||
<div class="footer-wrap">
|
||||
<div v-if="showInputTextArea" class="input-wrap">
|
||||
<ChatFooter />
|
||||
</div>
|
||||
<transition
|
||||
enter-active-class="transition-all delay-300 duration-300 ease"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
enter-class="opacity-0 transform translate-y-32"
|
||||
enter-to-class="opacity-100 transform translate-y-0"
|
||||
leave-class="opacity-100 transform translate-y-0"
|
||||
leave-to-class="opacity-0 transform translate-y-32 "
|
||||
>
|
||||
<div v-if="showInputTextArea && isOnMessageView" class="input-wrap">
|
||||
<chat-footer />
|
||||
</div>
|
||||
<team-availability
|
||||
v-if="!isOnMessageView"
|
||||
:available-agents="availableAgents"
|
||||
@start-conversation="startConversation"
|
||||
/>
|
||||
</transition>
|
||||
<branding></branding>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,18 +61,21 @@ import ChatFooter from 'widget/components/ChatFooter.vue';
|
||||
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
|
||||
import ChatHeader from 'widget/components/ChatHeader.vue';
|
||||
import ConversationWrap from 'widget/components/ConversationWrap.vue';
|
||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import TeamAvailability from 'widget/components/TeamAvailability';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
Branding,
|
||||
ChatFooter,
|
||||
ChatHeader,
|
||||
ChatHeaderExpanded,
|
||||
ConversationWrap,
|
||||
ChatHeader,
|
||||
Branding,
|
||||
AvailableAgents,
|
||||
Spinner,
|
||||
TeamAvailability,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
props: {
|
||||
@@ -67,16 +99,20 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
unreadMessageCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showPopoutButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMessageView: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFetchingList: 'conversation/getIsFetchingList',
|
||||
}),
|
||||
isOpen() {
|
||||
return this.conversationAttributes.status === 'open';
|
||||
},
|
||||
@@ -89,12 +125,18 @@ export default {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isOnMessageView() {
|
||||
if (this.hideWelcomeHeader) {
|
||||
return true;
|
||||
}
|
||||
if (this.conversationSize === 0) {
|
||||
return this.showMessageView;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isHeaderExpanded() {
|
||||
return this.conversationSize === 0;
|
||||
},
|
||||
showAvailableAgents() {
|
||||
return this.availableAgents.length > 0 && this.conversationSize < 1;
|
||||
},
|
||||
introHeading() {
|
||||
return this.channelConfig.welcomeTitle;
|
||||
},
|
||||
@@ -105,11 +147,16 @@ export default {
|
||||
return !(this.introHeading || this.introBody);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startConversation() {
|
||||
this.showMessageView = !this.showMessageView;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/woot.scss';
|
||||
@import '~widget/assets/scss/variables';
|
||||
|
||||
.home {
|
||||
width: 100%;
|
||||
@@ -117,14 +164,13 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
background: $color-background;
|
||||
|
||||
.header-wrap {
|
||||
flex-shrink: 0;
|
||||
border-radius: $space-normal $space-normal $space-small $space-small;
|
||||
background: white;
|
||||
z-index: 99;
|
||||
@include shadow-large;
|
||||
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 667px) {
|
||||
border-radius: 0;
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
|
||||
import { IFrameHelper } from 'widget/helpers/utils';
|
||||
import AgentBubble from 'widget/components/AgentMessageBubble.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
@@ -94,7 +92,8 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '~widget/assets/scss/woot.scss';
|
||||
@import '~widget/assets/scss/variables';
|
||||
|
||||
.unread-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -148,7 +147,7 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/woot.scss';
|
||||
@import '~widget/assets/scss/variables';
|
||||
|
||||
.unread-messages {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user