chore: Add Chatwoot SDK to Chatwoot Dashboard (#1726)
Add Chatwoot SDK to Chatwoot Dashboard
This commit is contained in:
parent
f46c4b5130
commit
d4c2a78db6
14 changed files with 481 additions and 266 deletions
|
@ -20,7 +20,8 @@ class DashboardController < ActionController::Base
|
||||||
'TERMS_URL',
|
'TERMS_URL',
|
||||||
'PRIVACY_URL',
|
'PRIVACY_URL',
|
||||||
'DISPLAY_MANIFEST',
|
'DISPLAY_MANIFEST',
|
||||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD'
|
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||||
|
'CHATWOOT_INBOX_TOKEN'
|
||||||
).merge(
|
).merge(
|
||||||
APP_VERSION: Chatwoot.config[:version]
|
APP_VERSION: Chatwoot.config[:version]
|
||||||
)
|
)
|
||||||
|
|
|
@ -72,9 +72,9 @@
|
||||||
.dropdown-pane {
|
.dropdown-pane {
|
||||||
@include elegant-card;
|
@include elegant-card;
|
||||||
@include border-light;
|
@include border-light;
|
||||||
|
bottom: 6rem;
|
||||||
display: block;
|
display: block;
|
||||||
left: 18%;
|
left: 5rem;
|
||||||
top: -110px;
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
@ -120,62 +120,6 @@
|
||||||
line-height: $global-lineheight;
|
line-height: $global-lineheight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-user {
|
|
||||||
@include flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.current-user--data {
|
|
||||||
@include flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.current-user--name {
|
|
||||||
font-size: $font-size-small;
|
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: $space-smaller;
|
|
||||||
margin-left: $space-one;
|
|
||||||
margin-top: $space-micro;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-user--role {
|
|
||||||
color: $color-gray;
|
|
||||||
font-size: $font-size-mini;
|
|
||||||
margin-bottom: $zero;
|
|
||||||
margin-left: $space-one;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-user--options {
|
|
||||||
font-size: $font-size-big;
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifications {
|
|
||||||
font-size: var(--font-size-big);
|
|
||||||
margin-bottom: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-top: auto;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.unread-badge {
|
|
||||||
background: var(--r-300);
|
|
||||||
border-radius: var(--space-small);
|
|
||||||
color: var(--white);
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
font-weight: var(--font-weight-black);
|
|
||||||
left: var(--space-slab);
|
|
||||||
padding: 0 var(--space-smaller);
|
|
||||||
position: absolute;
|
|
||||||
top: var(--space-smaller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hamburger--menu {
|
.hamburger--menu {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -30,180 +30,65 @@
|
||||||
<availability-status />
|
<availability-status />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-nav app-context-menu">
|
<div class="bottom-nav app-context-menu" @click="toggleOptions">
|
||||||
<transition name="menu-slide">
|
<agent-details @show-options="toggleOptions" />
|
||||||
<div
|
<notification-bell />
|
||||||
v-if="showOptionsMenu"
|
<span class="current-user--options icon ion-android-more-vertical" />
|
||||||
v-on-clickaway="showOptions"
|
<options-menu
|
||||||
class="dropdown-pane top"
|
:show="showOptionsMenu"
|
||||||
>
|
@toggle-accounts="toggleAccountModal"
|
||||||
<ul class="vertical dropdown menu">
|
@show-support-chat-window="toggleSupportChatWindow"
|
||||||
<li v-if="showChangeAccountOption">
|
@close="toggleOptions"
|
||||||
<button
|
|
||||||
class="button clear change-accounts--button"
|
|
||||||
@click="changeAccount"
|
|
||||||
>
|
|
||||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
|
||||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" @click.prevent="logout()">
|
|
||||||
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<div class="current-user" @click.prevent="showOptions()">
|
|
||||||
<thumbnail
|
|
||||||
:src="currentUser.avatar_url"
|
|
||||||
:username="currentUserAvailableName"
|
|
||||||
/>
|
|
||||||
<div class="current-user--data">
|
|
||||||
<h3 class="current-user--name">
|
|
||||||
{{ currentUserAvailableName }}
|
|
||||||
</h3>
|
|
||||||
<h5 v-if="currentRole" class="current-user--role">
|
|
||||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="notifications icon ion-ios-bell"
|
|
||||||
@click.stop="showNotification"
|
|
||||||
>
|
|
||||||
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="current-user--options icon ion-android-more-vertical" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<woot-modal
|
|
||||||
:show="showAccountModal"
|
|
||||||
:on-close="onClose"
|
|
||||||
class="account-selector--modal"
|
|
||||||
>
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
|
||||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
|
||||||
/>
|
/>
|
||||||
<div
|
</div>
|
||||||
v-for="account in currentUser.accounts"
|
|
||||||
:key="account.id"
|
|
||||||
class="account-selector"
|
|
||||||
>
|
|
||||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
|
||||||
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
|
||||||
<label :for="account.name" class="account--details">
|
|
||||||
<div class="account--name">{{ account.name }}</div>
|
|
||||||
<div class="account--role">{{ account.role }}</div>
|
|
||||||
</label>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="globalConfig.createNewAccountFromDashboard"
|
|
||||||
class="modal-footer delete-item"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="button success large expanded nice"
|
|
||||||
@click="createAccount"
|
|
||||||
>
|
|
||||||
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</woot-modal>
|
|
||||||
|
|
||||||
<woot-modal
|
<account-selector
|
||||||
|
:show-account-modal="showAccountModal"
|
||||||
|
@close-account-modal="toggleAccountModal"
|
||||||
|
@show-create-account-modal="openCreateAccountModal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<add-account-modal
|
||||||
:show="showCreateAccountModal"
|
:show="showCreateAccountModal"
|
||||||
:on-close="onCloseCreate"
|
@close-account-create-modal="closeCreateAccountModal"
|
||||||
class="account-selector--modal"
|
/>
|
||||||
>
|
|
||||||
<div class="column content-box">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
|
||||||
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form class="row" @submit.prevent="addAccount()">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<label :class="{ error: $v.accountName.$error }">
|
|
||||||
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model.trim="accountName"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
|
|
||||||
@input="$v.accountName.$touch"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer medium-12 columns">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<woot-submit-button
|
|
||||||
:disabled="
|
|
||||||
$v.accountName.$invalid ||
|
|
||||||
$v.accountName.$invalid ||
|
|
||||||
uiFlags.isCreating
|
|
||||||
"
|
|
||||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
|
||||||
:loading="uiFlags.isCreating"
|
|
||||||
button-class="large expanded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</woot-modal>
|
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
|
||||||
|
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
import adminMixin from '../../mixins/isAdmin';
|
||||||
import Auth from '../../api/auth';
|
|
||||||
import SidebarItem from './SidebarItem';
|
import SidebarItem from './SidebarItem';
|
||||||
import AvailabilityStatus from './AvailabilityStatus';
|
import AvailabilityStatus from './AvailabilityStatus';
|
||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../helper/URLHelper';
|
||||||
import Thumbnail from '../widgets/Thumbnail';
|
|
||||||
import { getSidebarItems } from '../../i18n/default-sidebar';
|
import { getSidebarItems } from '../../i18n/default-sidebar';
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import NotificationBell from './sidebarComponents/NotificationBell';
|
||||||
|
import AgentDetails from './sidebarComponents/AgentDetails.vue';
|
||||||
|
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
||||||
|
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||||
|
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
AgentDetails,
|
||||||
SidebarItem,
|
SidebarItem,
|
||||||
Thumbnail,
|
|
||||||
AvailabilityStatus,
|
AvailabilityStatus,
|
||||||
|
NotificationBell,
|
||||||
|
OptionsMenu,
|
||||||
|
AccountSelector,
|
||||||
|
AddAccountModal,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, adminMixin, alertMixin],
|
mixins: [adminMixin, alertMixin],
|
||||||
props: {
|
|
||||||
route: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptionsMenu: false,
|
showOptionsMenu: false,
|
||||||
showAccountModal: false,
|
showAccountModal: false,
|
||||||
showCreateAccountModal: false,
|
showCreateAccountModal: false,
|
||||||
accountName: '',
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center',
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
|
||||||
accountName: {
|
|
||||||
required,
|
|
||||||
minLength: minLength(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
|
@ -211,19 +96,9 @@ export default {
|
||||||
inboxes: 'inboxes/getInboxes',
|
inboxes: 'inboxes/getInboxes',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
currentRole: 'getCurrentRole',
|
currentRole: 'getCurrentRole',
|
||||||
uiFlags: 'agents/getUIFlags',
|
|
||||||
accountLabels: 'labels/getLabelsOnSidebar',
|
accountLabels: 'labels/getLabelsOnSidebar',
|
||||||
notificationMetadata: 'notifications/getMeta',
|
|
||||||
}),
|
}),
|
||||||
currentUserAvailableName() {
|
|
||||||
return this.currentUser.name;
|
|
||||||
},
|
|
||||||
showChangeAccountOption() {
|
|
||||||
if (this.globalConfig.createNewAccountFromDashboard) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.currentUser.accounts.length > 1;
|
|
||||||
},
|
|
||||||
sidemenuItems() {
|
sidemenuItems() {
|
||||||
return getSidebarItems(this.accountId);
|
return getSidebarItems(this.accountId);
|
||||||
},
|
},
|
||||||
|
@ -292,22 +167,35 @@ export default {
|
||||||
dashboardPath() {
|
dashboardPath() {
|
||||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||||
},
|
},
|
||||||
unreadCount() {
|
},
|
||||||
if (!this.notificationMetadata.unreadCount) {
|
watch: {
|
||||||
return 0;
|
currentUser(newUserInfo, oldUserInfo) {
|
||||||
|
if (!oldUserInfo.email && newUserInfo.email) {
|
||||||
|
this.setChatwootUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notificationMetadata.unreadCount < 100
|
|
||||||
? this.notificationMetadata.unreadCount
|
|
||||||
: '99+';
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('labels/get');
|
this.$store.dispatch('labels/get');
|
||||||
this.$store.dispatch('inboxes/get');
|
this.$store.dispatch('inboxes/get');
|
||||||
this.$store.dispatch('notifications/unReadCount');
|
this.$store.dispatch('notifications/unReadCount');
|
||||||
|
this.setChatwootUser();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleSupportChatWindow() {
|
||||||
|
window.$chatwoot.toggle();
|
||||||
|
},
|
||||||
|
setChatwootUser() {
|
||||||
|
if (!this.currentUser.email || !this.globalConfig.chatwootInboxToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.$chatwoot.setUser(this.currentUser.email, {
|
||||||
|
name: this.currentUser.name,
|
||||||
|
email: this.currentUser.email,
|
||||||
|
avatar_url: this.currentUser.avatar_url,
|
||||||
|
identifier_hash: this.currentUser.hmac_identifier,
|
||||||
|
});
|
||||||
|
},
|
||||||
filterMenuItemsByRole(menuItems) {
|
filterMenuItemsByRole(menuItems) {
|
||||||
if (!this.currentRole) {
|
if (!this.currentRole) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -319,44 +207,19 @@ export default {
|
||||||
) > -1
|
) > -1
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
logout() {
|
toggleOptions() {
|
||||||
Auth.logout();
|
|
||||||
},
|
|
||||||
showOptions() {
|
|
||||||
this.showOptionsMenu = !this.showOptionsMenu;
|
this.showOptionsMenu = !this.showOptionsMenu;
|
||||||
},
|
},
|
||||||
showNotification() {
|
toggleAccountModal() {
|
||||||
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
|
this.showAccountModal = !this.showAccountModal;
|
||||||
},
|
},
|
||||||
changeAccount() {
|
openCreateAccountModal() {
|
||||||
this.showAccountModal = true;
|
|
||||||
},
|
|
||||||
onClose() {
|
|
||||||
this.showAccountModal = false;
|
|
||||||
},
|
|
||||||
createAccount() {
|
|
||||||
this.showAccountModal = false;
|
this.showAccountModal = false;
|
||||||
this.showCreateAccountModal = true;
|
this.showCreateAccountModal = true;
|
||||||
},
|
},
|
||||||
onCloseCreate() {
|
closeCreateAccountModal() {
|
||||||
this.showCreateAccountModal = false;
|
this.showCreateAccountModal = false;
|
||||||
},
|
},
|
||||||
async addAccount() {
|
|
||||||
try {
|
|
||||||
const account_id = await this.$store.dispatch('accounts/create', {
|
|
||||||
account_name: this.accountName,
|
|
||||||
});
|
|
||||||
this.onClose();
|
|
||||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
|
||||||
window.location = `/app/accounts/${account_id}/dashboard`;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response.status === 422) {
|
|
||||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
|
||||||
} else {
|
|
||||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -427,7 +290,19 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-context-menu {
|
.app-context-menu {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-user--options {
|
||||||
|
font-size: $font-size-big;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<woot-modal
|
||||||
|
:show="showAccountModal"
|
||||||
|
:on-close="() => $emit('close-account-modal')"
|
||||||
|
class="account-selector--modal"
|
||||||
|
>
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||||
|
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-for="account in currentUser.accounts"
|
||||||
|
:key="account.id"
|
||||||
|
class="account-selector"
|
||||||
|
>
|
||||||
|
<a :href="`/app/accounts/${account.id}/dashboard`">
|
||||||
|
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
||||||
|
<label :for="account.name" class="account--details">
|
||||||
|
<div class="account--name">{{ account.name }}</div>
|
||||||
|
<div class="account--role">{{ account.role }}</div>
|
||||||
|
</label>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="globalConfig.createNewAccountFromDashboard"
|
||||||
|
class="modal-footer delete-item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button success large expanded nice"
|
||||||
|
@click="$emit('show-create-account-modal')"
|
||||||
|
>
|
||||||
|
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</woot-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
showAccountModal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
|
globalConfig: 'globalConfig/get',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<woot-modal
|
||||||
|
:show="show"
|
||||||
|
:on-close="() => $emit('close-account-create-modal')"
|
||||||
|
class="account-selector--modal"
|
||||||
|
>
|
||||||
|
<div class="column content-box">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||||
|
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form class="row" @submit.prevent="addAccount">
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<label :class="{ error: $v.accountName.$error }">
|
||||||
|
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="accountName"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
|
||||||
|
@input="$v.accountName.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer medium-12 columns">
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<woot-submit-button
|
||||||
|
:disabled="
|
||||||
|
$v.accountName.$invalid ||
|
||||||
|
$v.accountName.$invalid ||
|
||||||
|
uiFlags.isCreating
|
||||||
|
"
|
||||||
|
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||||
|
:loading="uiFlags.isCreating"
|
||||||
|
button-class="large expanded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</woot-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
accountName: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
accountName: {
|
||||||
|
required,
|
||||||
|
minLength: minLength(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'agents/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async addAccount() {
|
||||||
|
try {
|
||||||
|
const account_id = await this.$store.dispatch('accounts/create', {
|
||||||
|
account_name: this.accountName,
|
||||||
|
});
|
||||||
|
this.$emit('close-account-create-modal');
|
||||||
|
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||||
|
window.location = `/app/accounts/${account_id}/dashboard`;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||||
|
} else {
|
||||||
|
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="current-user--row">
|
||||||
|
<thumbnail
|
||||||
|
:src="currentUser.avatar_url"
|
||||||
|
:username="currentUserAvailableName"
|
||||||
|
/>
|
||||||
|
<div class="current-user--data">
|
||||||
|
<h3 class="current-user--name text-truncate">
|
||||||
|
{{ currentUserAvailableName }}
|
||||||
|
</h3>
|
||||||
|
<h5 v-if="currentRole" class="current-user--role">
|
||||||
|
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Thumbnail from '../../widgets/Thumbnail';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
|
currentRole: 'getCurrentRole',
|
||||||
|
}),
|
||||||
|
currentUserAvailableName() {
|
||||||
|
return this.currentUser.name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.current-user--row {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-user--data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.current-user--name {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
margin-bottom: var(--space-micro);
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
max-width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-user--role {
|
||||||
|
color: var(--color-gray);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin-bottom: var(--zero);
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<span class="notifications icon ion-ios-bell" @click.stop="showNotification">
|
||||||
|
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
notificationMetadata: 'notifications/getMeta',
|
||||||
|
}),
|
||||||
|
unreadCount() {
|
||||||
|
if (!this.notificationMetadata.unreadCount) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.notificationMetadata.unreadCount < 100
|
||||||
|
? this.notificationMetadata.unreadCount
|
||||||
|
: '99+';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showNotification() {
|
||||||
|
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.notifications {
|
||||||
|
font-size: var(--font-size-big);
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.unread-badge {
|
||||||
|
background: var(--r-300);
|
||||||
|
border-radius: var(--space-small);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
font-weight: var(--font-weight-black);
|
||||||
|
left: var(--space-slab);
|
||||||
|
padding: 0 var(--space-smaller);
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-smaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<transition name="menu-slide">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
v-on-clickaway="() => $emit('close')"
|
||||||
|
class="dropdown-pane top"
|
||||||
|
>
|
||||||
|
<ul class="vertical dropdown menu">
|
||||||
|
<li v-if="showChangeAccountOption">
|
||||||
|
<button
|
||||||
|
class="button clear change-accounts--button"
|
||||||
|
@click="$emit('toggle-accounts')"
|
||||||
|
>
|
||||||
|
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li v-if="globalConfig.chatwootInboxToken">
|
||||||
|
<button
|
||||||
|
class="button clear change-accounts--button"
|
||||||
|
@click="$emit('show-support-chat-window')"
|
||||||
|
>
|
||||||
|
Contact Support
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
||||||
|
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" @click.prevent="logout">
|
||||||
|
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Auth from '../../../api/auth';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [clickaway],
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
|
globalConfig: 'globalConfig/get',
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
}),
|
||||||
|
showChangeAccountOption() {
|
||||||
|
if (this.globalConfig.createNewAccountFromDashboard) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.currentUser.accounts.length > 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
logout() {
|
||||||
|
Auth.logout();
|
||||||
|
|
||||||
|
if (this.globalConfig.chatwootInboxToken) {
|
||||||
|
window.$chatwoot.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,6 +1,7 @@
|
||||||
const {
|
const {
|
||||||
APP_VERSION: appVersion,
|
APP_VERSION: appVersion,
|
||||||
BRAND_NAME: brandName,
|
BRAND_NAME: brandName,
|
||||||
|
CHATWOOT_INBOX_TOKEN: chatwootInboxToken,
|
||||||
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
||||||
DISPLAY_MANIFEST: displayManifest,
|
DISPLAY_MANIFEST: displayManifest,
|
||||||
INSTALLATION_NAME: installationName,
|
INSTALLATION_NAME: installationName,
|
||||||
|
@ -14,6 +15,7 @@ const {
|
||||||
const state = {
|
const state = {
|
||||||
appVersion,
|
appVersion,
|
||||||
brandName,
|
brandName,
|
||||||
|
chatwootInboxToken,
|
||||||
createNewAccountFromDashboard,
|
createNewAccountFromDashboard,
|
||||||
displayManifest,
|
displayManifest,
|
||||||
installationName,
|
installationName,
|
||||||
|
|
|
@ -109,6 +109,13 @@ class User < ApplicationRecord
|
||||||
self[:display_name].presence || name
|
self[:display_name].presence || name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hmac_identifier
|
||||||
|
hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY']
|
||||||
|
return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present?
|
||||||
|
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
current_account_user&.account
|
current_account_user&.account
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
json.id resource.id
|
json.access_token resource.access_token.token
|
||||||
json.provider resource.provider
|
|
||||||
json.uid resource.uid
|
|
||||||
json.name resource.name
|
|
||||||
json.display_name resource.display_name
|
|
||||||
json.available_name resource.available_name
|
|
||||||
json.email resource.email
|
|
||||||
json.account_id resource.active_account_user&.account_id
|
json.account_id resource.active_account_user&.account_id
|
||||||
|
json.availability_status resource.availability_status
|
||||||
|
json.available_name resource.available_name
|
||||||
|
json.avatar_url resource.avatar_url
|
||||||
|
json.confirmed resource.confirmed?
|
||||||
|
json.display_name resource.display_name
|
||||||
|
json.email resource.email
|
||||||
|
json.hmac_identifier resource.hmac_identifier if GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY'].present?
|
||||||
|
json.id resource.id
|
||||||
|
json.inviter_id resource.active_account_user&.inviter_id
|
||||||
|
json.name resource.name
|
||||||
|
json.provider resource.provider
|
||||||
json.pubsub_token resource.pubsub_token
|
json.pubsub_token resource.pubsub_token
|
||||||
json.role resource.active_account_user&.role
|
json.role resource.active_account_user&.role
|
||||||
json.inviter_id resource.active_account_user&.inviter_id
|
|
||||||
json.confirmed resource.confirmed?
|
|
||||||
json.avatar_url resource.avatar_url
|
|
||||||
json.access_token resource.access_token.token
|
|
||||||
json.availability_status resource.availability_status
|
|
||||||
json.ui_settings resource.ui_settings
|
json.ui_settings resource.ui_settings
|
||||||
|
json.uid resource.uid
|
||||||
json.accounts do
|
json.accounts do
|
||||||
json.array! resource.account_users do |account_user|
|
json.array! resource.account_users do |account_user|
|
||||||
json.id account_user.account_id
|
json.id account_user.account_id
|
||||||
|
|
|
@ -51,5 +51,27 @@
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<noscript id="noscript">This app works best with JavaScript enabled.</noscript>
|
<noscript id="noscript">This app works best with JavaScript enabled.</noscript>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
|
<% @global_config['CHATWOOT_INBOX_TOKEN'] %>
|
||||||
|
<% if @global_config['CHATWOOT_INBOX_TOKEN'] %>
|
||||||
|
<script>
|
||||||
|
window.chatwootSettings = {
|
||||||
|
hideMessageBubble: true,
|
||||||
|
position: 'left',
|
||||||
|
};
|
||||||
|
(function(d,t) {
|
||||||
|
var BASE_URL="";
|
||||||
|
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||||
|
g.src=BASE_URL+"/packs/js/sdk.js";
|
||||||
|
s.parentNode.insertBefore(g,s);
|
||||||
|
s.async=!0;
|
||||||
|
g.onload=function(){
|
||||||
|
window.chatwootSDK.run({
|
||||||
|
websiteToken: '<%= @global_config['CHATWOOT_INBOX_TOKEN'] %>',
|
||||||
|
baseUrl: BASE_URL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})(document,"script");
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -28,5 +28,11 @@
|
||||||
value: false
|
value: false
|
||||||
locked: false
|
locked: false
|
||||||
- name: INSTALLATION_EVENTS_WEBHOOK_URL
|
- name: INSTALLATION_EVENTS_WEBHOOK_URL
|
||||||
value:
|
value:
|
||||||
|
locked: false
|
||||||
|
- name: CHATWOOT_INBOX_TOKEN
|
||||||
|
value:
|
||||||
|
locked: false
|
||||||
|
- name: CHATWOOT_INBOX_HMAC_KEY
|
||||||
|
value:
|
||||||
locked: false
|
locked: false
|
||||||
|
|
|
@ -34,6 +34,24 @@ RSpec.describe User do
|
||||||
it { expect(user.saved_changes.keys).not_to eq('pubsub_token') }
|
it { expect(user.saved_changes.keys).not_to eq('pubsub_token') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'hmac_identifier' do
|
||||||
|
it 'return nil if CHATWOOT_INBOX_HMAC_KEY is not set' do
|
||||||
|
expect(user.hmac_identifier).to eq('')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return value if CHATWOOT_INBOX_HMAC_KEY is set' do
|
||||||
|
ConfigLoader.new.process
|
||||||
|
i = InstallationConfig.find_by(name: 'CHATWOOT_INBOX_HMAC_KEY')
|
||||||
|
i.value = 'random_secret_key'
|
||||||
|
i.save!
|
||||||
|
GlobalConfig.clear_cache
|
||||||
|
|
||||||
|
expected_hmac_identifier = OpenSSL::HMAC.hexdigest('sha256', 'random_secret_key', user.email)
|
||||||
|
|
||||||
|
expect(user.hmac_identifier).to eq expected_hmac_identifier
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'sso_auth_token' do
|
context 'sso_auth_token' do
|
||||||
it 'can generate multiple sso tokens which can be validated' do
|
it 'can generate multiple sso tokens which can be validated' do
|
||||||
sso_auth_token1 = user.generate_sso_auth_token
|
sso_auth_token1 = user.generate_sso_auth_token
|
||||||
|
|
Loading…
Reference in a new issue