Refactor: Inbox store, remove inboxes from sidebar (#387)

* Refactor: Inbox store, remove inboxes from sidebar

* Add a new page for inbox settings

* Show inboxes on sidebar

* Add inbox_members API

* Disable similar-code check

* Fix codeclimate scss issues

* Add widget_color update API and actions

* Add specs for inbox store

* Fix Facebook auth flow

* Fix agent loading, inbox name
This commit is contained in:
Pranav Raj S 2019-12-28 21:56:42 +05:30 committed by Sojan Jose
parent 96f8070e79
commit 5ddc46c474
51 changed files with 1028 additions and 726 deletions

View file

@ -11,7 +11,9 @@ plugins:
enabled: true
brakeman:
enabled: true
checks:
similar-code:
enabled: false
exclude_patterns:
- "spec/"
- "**/specs/"

View file

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

View file

@ -4,7 +4,8 @@ const API_VERSION = `/api/v1`;
class ApiClient {
constructor(url) {
this.url = `${API_VERSION}/${url}`;
this.apiVersion = API_VERSION;
this.url = `${this.apiVersion}/${url}`;
}
get() {

View file

@ -1,32 +0,0 @@
/* global axios */
import endPoints from './endPoints';
export default {
getLabels() {
const urlData = endPoints('fetchLabels');
return axios.get(urlData.url);
},
getInboxes() {
const urlData = endPoints('fetchInboxes');
return axios.get(urlData.url);
},
deleteInbox(id) {
const urlData = endPoints('inbox').delete(id);
return axios.delete(urlData.url);
},
listInboxAgents(id) {
const urlData = endPoints('inbox').agents.get(id);
return axios.get(urlData.url);
},
updateInboxAgents(inboxId, agentList) {
const urlData = endPoints('inbox').agents.post();
return axios.post(urlData.url, {
user_ids: agentList,
inbox_id: inboxId,
});
},
};

View file

@ -19,6 +19,13 @@ class FBChannel extends ApiClient {
contact_id: contactId,
});
}
create(params) {
return axios.post(
`${this.apiVersion}/callbacks/register_facebook_page`,
params
);
}
}
export default new FBChannel();

View file

@ -5,19 +5,6 @@
import endPoints from './endPoints';
export default {
// Get Inbox related to the account
createChannel(channel, channelParams) {
const urlData = endPoints('createChannel')(channel, channelParams);
return axios.post(urlData.url, urlData.params);
},
addAgentsToChannel(inboxId, agentsId) {
const urlData = endPoints('addAgentsToChannel');
urlData.params.inbox_id = inboxId;
urlData.params.user_ids = agentsId;
return axios.post(urlData.url, urlData.params);
},
fetchFacebookPages(token) {
const urlData = endPoints('fetchFacebookPages');
urlData.params.omniauth_token = token;

View file

@ -24,26 +24,6 @@ const endPoints = {
params: { inbox_id: null },
},
fetchLabels: {
url: 'api/v1/labels.json',
},
fetchInboxes: {
url: 'api/v1/inboxes.json',
},
createChannel(channel, channelParams) {
return {
url: `api/v1/callbacks/register_${channel}_page.json`,
params: channelParams,
};
},
addAgentsToChannel: {
url: 'api/v1/inbox_members.json',
params: { user_ids: [], inbox_id: null },
},
fetchFacebookPages: {
url: 'api/v1/callbacks/get_facebook_pages.json',
params: { omniauth_token: '' },
@ -69,26 +49,6 @@ const endPoints = {
};
},
},
inbox: {
delete(id) {
return {
url: `/api/v1/inboxes/${id}`,
};
},
agents: {
get(id) {
return {
url: `/api/v1/inbox_members/${id}.json`,
};
},
post() {
return {
url: '/api/v1/inbox_members.json',
};
},
},
},
};
export default page => {

View file

@ -0,0 +1,17 @@
/* global axios */
import ApiClient from './ApiClient';
class InboxMembers extends ApiClient {
constructor() {
super('inbox_members');
}
create({ inboxId, agentList }) {
return axios.post(this.url, {
inbox_id: inboxId,
user_ids: agentList,
});
}
}
export default new InboxMembers();

View file

@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class Inboxes extends ApiClient {
constructor() {
super('inboxes');
}
}
export default new Inboxes();

View file

@ -0,0 +1,13 @@
import inboxes from '../inboxes';
import ApiClient from '../ApiClient';
describe('#AgentAPI', () => {
it('creates correct instance', () => {
expect(inboxes).toBeInstanceOf(ApiClient);
expect(inboxes).toHaveProperty('get');
expect(inboxes).toHaveProperty('show');
expect(inboxes).toHaveProperty('create');
expect(inboxes).toHaveProperty('update');
expect(inboxes).toHaveProperty('delete');
});
});

View file

@ -1,3 +1,10 @@
.settings {
overflow: auto;
.page-top-bar {
@include padding($space-normal $space-two $zero);
}
}
// Conversation header - Light BG
.settings-header {
@include padding($space-small $space-normal);
@ -196,51 +203,23 @@
}
}
.settings-modal {
height: 80%;
max-width: 1040px;
width: 100%;
.settings--content {
@include margin($space-small $space-medium);
.delete-wrapper {
position: absolute;
bottom: 0;
width: 100%;
@include flex;
flex-direction: row;
justify-content: space-between;
@include padding($space-normal $space-large);
a {
margin-left: $space-normal;
}
.title {
font-weight: $font-weight-medium;
}
.settings--content {
@include margin($space-medium);
.code {
max-height: $space-mega;
overflow: scroll;
white-space: nowrap;
@include padding($space-one);
background: $color-background;
.title {
font-weight: $font-weight-medium;
}
.code {
max-height: $space-mega;
overflow: scroll;
white-space: nowrap;
@include padding($space-one);
background: $color-background;
code {
background: transparent;
border: 0;
}
}
}
.agent-wrapper {
@include margin($space-medium);
.title {
font-weight: $font-weight-medium;
code {
background: transparent;
border: 0;
}
}
}

View file

@ -27,6 +27,15 @@
}
}
.page-top-bar {
@include padding($zero $space-two);
img {
max-height: 6rem;
}
}
.modal-container {
background-color: $color-white;
border-radius: $space-small;
@ -35,13 +44,6 @@
position: relative;
width: 60rem;
.page-top-bar {
@include padding($zero $space-two);
img {
max-height: 6rem;
}
}
.content-box {
@include padding($zero);

View file

@ -3,7 +3,7 @@
<div class="chat-list__top">
<h1 class="page-title">
<woot-sidemenu-icon />
{{ getInboxName }}
{{ inbox.name || pageTitle }}
</h1>
<chat-filter @statusFilterChange="getDataForStatusTab" />
</div>
@ -53,6 +53,11 @@ import conversationMixin from '../mixins/conversations';
import wootConstants from '../constants';
export default {
components: {
ChatTypeTabs,
ConversationCard,
ChatFilter,
},
mixins: [timeMixin, conversationMixin],
props: ['conversationInbox', 'pageTitle'],
data() {
@ -61,25 +66,12 @@ export default {
activeStatus: 0,
};
},
mounted() {
this.$watch('$store.state.route', () => {
if (this.$store.state.route.name !== 'inbox_conversation') {
this.$store.dispatch('emptyAllConversations');
this.fetchData();
}
});
this.$store.dispatch('emptyAllConversations');
this.fetchData();
this.$store.dispatch('agents/get');
},
computed: {
...mapGetters({
chatLists: 'getAllConversations',
mineChatsList: 'getMineChats',
allChatList: 'getAllStatusChats',
unAssignedChatsList: 'getUnAssignedChats',
inboxesList: 'getInboxesList',
chatListLoading: 'getChatListLoadingStatus',
currentUserID: 'getCurrentUserID',
activeInbox: 'getSelectedInbox',
@ -92,12 +84,8 @@ export default {
count: this.convStats[item.KEY] || 0,
}));
},
getInboxName() {
const inboxId = Number(this.activeInbox);
const [stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
return !stateInbox ? this.pageTitle : stateInbox.label;
inbox() {
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
},
getToggleStatus() {
if (this.toggleType) {
@ -106,6 +94,18 @@ export default {
return 'Resolved';
},
},
mounted() {
this.$watch('$store.state.route', () => {
if (this.$store.state.route.name !== 'inbox_conversation') {
this.$store.dispatch('emptyAllConversations');
this.fetchData();
}
});
this.$store.dispatch('emptyAllConversations');
this.fetchData();
this.$store.dispatch('agents/get');
},
methods: {
fetchData() {
if (this.chatLists.length === 0) {
@ -149,12 +149,6 @@ export default {
return sorted;
},
},
components: {
ChatTypeTabs,
ConversationCard,
ChatFilter,
},
};
</script>

View file

@ -1,11 +1,11 @@
<template>
<div class="column page-top-bar">
<img :src="headerImage" alt="No image" v-if="headerImage"/>
<img v-if="headerImage" :src="headerImage" alt="No image" />
<h2 class="page-sub-title">
{{headerTitle}}
{{ headerTitle }}
</h2>
<p class="small-12 column">
{{headerContent}}
<p v-if="headerContent" class="small-12 column">
{{ headerContent }}
</p>
</div>
</template>

View file

@ -10,7 +10,7 @@
</div>
<div v-if="buttonText" class="medium-4 text-right">
<woot-submit-button
class="small"
class="default"
:button-text="buttonText"
:loading="isUpdating"
@click="onClick()"
@ -32,7 +32,7 @@ export default {
},
buttonText: {
type: String,
required: true,
default: '',
},
isUpdating: {
type: Boolean,
@ -51,8 +51,19 @@ export default {
@import '~dashboard/assets/scss/variables';
.settings--form--header {
align-items: center;
border-bottom: 1px solid $color-border;
display: flex;
margin-bottom: $space-normal;
padding: $space-normal 0;
.button {
margin-bottom: 0;
}
.title {
margin-bottom: 0;
font-size: $font-size-default;
}
}
</style>

View file

@ -13,6 +13,12 @@
:key="item.toState"
:menu-item="item"
/>
<sidebar-item
v-if="shouldShowInboxes"
:key="inboxSection.toState"
:menu-item="inboxSection"
/>
</transition-group>
</div>
@ -41,7 +47,7 @@
</div>
</transition>
<div class="current-user" @click.prevent="showOptions()">
<thumbnail :src="gravatarUrl()" :username="currentUser.name"/>
<thumbnail :src="gravatarUrl()" :username="currentUser.name" />
<div class="current-user--data">
<h3 class="current-user--name">
{{ currentUser.name }}
@ -50,9 +56,8 @@
{{ currentUser.role }}
</h5>
</div>
<span
class="current-user--options icon ion-android-more-vertical"
></span>
<span class="current-user--options icon ion-android-more-vertical">
</span>
</div>
</div>
</aside>
@ -69,12 +74,19 @@ import SidebarItem from './SidebarItem';
import WootStatusBar from '../widgets/StatusBar';
import { frontendURL } from '../../helper/URLHelper';
import Thumbnail from '../widgets/Thumbnail';
import sidemenuItems from '../../i18n/default-sidebar';
export default {
components: {
SidebarItem,
WootStatusBar,
Thumbnail,
},
mixins: [clickaway, adminMixin],
props: {
route: {
type: String,
default: '',
},
},
data() {
@ -82,27 +94,22 @@ export default {
showOptionsMenu: false,
};
},
mounted() {
// this.$store.dispatch('fetchLabels');
this.$store.dispatch('fetchInboxes');
},
computed: {
...mapGetters({
sidebarList: 'getMenuItems',
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
inboxes: 'inboxes/getInboxes',
}),
accessibleMenuItems() {
const currentRoute = this.$store.state.route.name;
// get all keys in menuGroup
const groupKey = Object.keys(this.sidebarList);
const groupKey = Object.keys(sidemenuItems);
let menuItems = [];
// Iterate over menuGroup to find the correct group
for (let i = 0; i < groupKey.length; i += 1) {
const groupItem = this.sidebarList[groupKey[i]];
const groupItem = sidemenuItems[groupKey[i]];
// Check if current route is included
const isRouteIncluded = groupItem.routes.includes(currentRoute);
const isRouteIncluded = groupItem.routes.includes(this.currentRoute);
if (isRouteIncluded) {
menuItems = Object.values(groupItem.menuItems);
}
@ -114,6 +121,29 @@ export default {
return this.filterMenuItemsByRole(menuItems);
},
currentRoute() {
return this.$store.state.route.name;
},
shouldShowInboxes() {
return sidemenuItems.common.routes.includes(this.currentRoute);
},
inboxSection() {
return {
icon: 'ion-folder',
label: 'Inboxes',
hasSubMenu: true,
newLink: true,
key: 'inbox',
cssClass: 'menu-title align-justify',
toState: frontendURL('settings/inboxes'),
toStateName: 'settings_inbox_list',
children: this.inboxes.map(inbox => ({
id: inbox.id,
label: inbox.name,
toState: frontendURL(`inbox/${inbox.id}`),
})),
};
},
currentUser() {
return Auth.getCurrentUser();
},
@ -140,7 +170,9 @@ export default {
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
},
},
mounted() {
this.$store.dispatch('inboxes/get');
},
methods: {
gravatarUrl() {
const hash = md5(this.currentUser.email);
@ -165,11 +197,5 @@ export default {
this.showOptionsMenu = !this.showOptionsMenu;
},
},
components: {
SidebarItem,
WootStatusBar,
Thumbnail,
},
};
</script>

View file

@ -63,7 +63,7 @@ export default {
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
inboxesList: 'getInboxesList',
inboxesList: 'inboxes/getInboxes',
activeInbox: 'getSelectedInbox',
}),
@ -107,11 +107,10 @@ export default {
`;
},
getEmojiSVG,
inboxName(inboxId) {
const [stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
return !stateInbox ? '' : stateInbox.label;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
return stateInbox.name || '';
},
},
};

View file

@ -1,11 +1,11 @@
<template>
<div class="columns full-height conv-empty-state">
<woot-loading-state
v-if="fetchingInboxes || loadingChatList"
v-if="uiFlags.isFetching || loadingChatList"
:message="loadingIndicatorMessage"
/>
<!-- Show empty state images if not loading -->
<div v-if="!fetchingInboxes && !loadingChatList" class="current-chat">
<div v-if="!uiFlags.isFetching && !loadingChatList" class="current-chat">
<!-- No inboxes attached -->
<div v-if="!inboxesList.length">
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
@ -49,12 +49,12 @@ export default {
...mapGetters({
currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'getInboxesList',
fetchingInboxes: 'getInboxLoadingStatus',
inboxesList: 'inboxes/getInboxes',
uiFlags: 'inboxes/getUIFlags',
loadingChatList: 'getChatListLoadingStatus',
}),
loadingIndicatorMessage() {
if (this.fetchingInboxes) {
if (this.uiFlags.isFetching) {
return this.$t('CONVERSATION.LOADING_INBOXES');
}
return this.$t('CONVERSATION.LOADING_CONVERSATIONS');

View file

@ -75,10 +75,9 @@ export default {
...mapGetters({
currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'getInboxesList',
inboxesList: 'inboxes/getInboxes',
listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount',
fetchingInboxes: 'getInboxLoadingStatus',
loadingChatList: 'getChatListLoadingStatus',
}),
@ -99,7 +98,7 @@ export default {
} else {
[stateInbox] = this.inboxesList;
}
return !stateInbox ? 0 : stateInbox.pageId;
return !stateInbox ? 0 : stateInbox.page_id;
},
// Get current FB Page ID link
linkToMessage() {

View file

@ -1,109 +1,97 @@
import { frontendURL } from '../helper/URLHelper';
export default {
menuGroup: {
common: {
routes: [
'home',
'inbox_dashboard',
'inbox_conversation',
'settings_account_reports',
'billing_deactivated',
],
menuItems: {
assignedToMe: {
icon: 'ion-chatbox-working',
label: 'Conversations',
hasSubMenu: false,
key: '',
toState: frontendURL('dashboard'),
toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home',
},
report: {
icon: 'ion-arrow-graph-up-right',
label: 'Reports',
hasSubMenu: false,
toState: frontendURL('reports'),
toStateName: 'settings_account_reports',
},
settings: {
icon: 'ion-settings',
label: 'Settings',
hasSubMenu: false,
toState: frontendURL('settings'),
toStateName: 'settings_home',
},
inbox: {
icon: 'ion-folder',
label: 'Inboxes',
hasSubMenu: true,
newLink: true,
key: 'inbox',
cssClass: 'menu-title align-justify',
toState: frontendURL('settings/inboxes'),
toStateName: 'settings_inbox_list',
children: [],
},
common: {
routes: [
'home',
'inbox_dashboard',
'inbox_conversation',
'settings_account_reports',
'billing_deactivated',
],
menuItems: {
assignedToMe: {
icon: 'ion-chatbox-working',
label: 'Conversations',
hasSubMenu: false,
key: '',
toState: frontendURL('dashboard'),
toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home',
},
report: {
icon: 'ion-arrow-graph-up-right',
label: 'Reports',
hasSubMenu: false,
toState: frontendURL('reports'),
toStateName: 'settings_account_reports',
},
settings: {
icon: 'ion-settings',
label: 'Settings',
hasSubMenu: false,
toState: frontendURL('settings'),
toStateName: 'settings_home',
},
},
settings: {
routes: [
'agent_list',
'agent_new',
'canned_list',
'canned_new',
'settings_inbox',
'settings_inbox_new',
'settings_inbox_list',
'settings_inboxes_page_channel',
'settings_inboxes_add_agents',
'settings_inbox_finish',
'billing',
],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'Home',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL('dashboard'),
},
agents: {
icon: 'ion-person-stalker',
label: 'Agents',
hasSubMenu: false,
toState: frontendURL('settings/agents/list'),
toStateName: 'agent_list',
},
inboxes: {
icon: 'ion-archive',
label: 'Inboxes',
hasSubMenu: false,
toState: frontendURL('settings/inboxes/list'),
toStateName: 'settings_inbox_list',
},
cannedResponses: {
icon: 'ion-chatbox-working',
label: 'Canned Responses',
hasSubMenu: false,
toState: frontendURL('settings/canned-response/list'),
toStateName: 'canned_list',
},
billing: {
icon: 'ion-card',
label: 'Billing',
hasSubMenu: false,
toState: frontendURL('settings/billing'),
toStateName: 'billing',
},
account: {
icon: 'ion-beer',
label: 'Account Settings',
hasSubMenu: false,
toState: frontendURL('settings/account'),
toStateName: 'account',
},
},
settings: {
routes: [
'agent_list',
'agent_new',
'canned_list',
'canned_new',
'settings_inbox',
'settings_inbox_new',
'settings_inbox_list',
'settings_inbox_show',
'settings_inboxes_page_channel',
'settings_inboxes_add_agents',
'settings_inbox_finish',
'billing',
],
menuItems: {
back: {
icon: 'ion-ios-arrow-back',
label: 'Home',
hasSubMenu: false,
toStateName: 'home',
toState: frontendURL('dashboard'),
},
agents: {
icon: 'ion-person-stalker',
label: 'Agents',
hasSubMenu: false,
toState: frontendURL('settings/agents/list'),
toStateName: 'agent_list',
},
inboxes: {
icon: 'ion-archive',
label: 'Inboxes',
hasSubMenu: false,
toState: frontendURL('settings/inboxes/list'),
toStateName: 'settings_inbox_list',
},
cannedResponses: {
icon: 'ion-chatbox-working',
label: 'Canned Responses',
hasSubMenu: false,
toState: frontendURL('settings/canned-response/list'),
toStateName: 'canned_list',
},
billing: {
icon: 'ion-card',
label: 'Billing',
hasSubMenu: false,
toState: frontendURL('settings/billing'),
toStateName: 'billing',
},
account: {
icon: 'ion-beer',
label: 'Account Settings',
hasSubMenu: false,
toState: frontendURL('settings/account'),
toStateName: 'account',
},
},
},

View file

@ -27,7 +27,8 @@
"PLACEHOLDER": "Enter your website domain (eg: acme.com)"
},
"WIDGET_COLOR": {
"LABEL": "Widget Color"
"LABEL": "Widget Color",
"PLACEHOLDER": "Update the widget color used in widget"
},
"SUBMIT_BUTTON":"Create inbox"
},
@ -52,10 +53,11 @@
"LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"CREATING_CHANNEL": "Creating your Inbox...",
"TITLE": "Configure Inbox Deatails",
"DESC": "an addendum to this post, you can absolutely support what Im doing by working with me at Reach by Creatomic. Get in touch: jon@creatomic.co for content, podcasts, marketing campaignswe do a lot and we do it well. If you can help me hit that monthly rev. target by letting me help you find more customers and make more money, thats a win win."
"TITLE": "Configure Inbox Details",
"DESC": ""
},
"AGENTS": {
"BUTTON_TEXT": "Add agents",
"ADD_AGENTS": "Adding Agents to your Inbox..."
},
"FINISH": {
@ -66,6 +68,12 @@
},
"REAUTH": "Reauthorize",
"VIEW": "View",
"EDIT": {
"API": {
"SUCCESS_MESSAGE": "Widget color updated successfully",
"ERROR_MESSAGE": "Could not update widget color. Please try again later."
}
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"CONFIRM": {

View file

@ -39,7 +39,6 @@ export default {
},
computed: {
...mapGetters({
menuItems: 'getMenuItems',
chatList: 'getAllConversations',
}),
},

View file

@ -1,10 +1,9 @@
<template>
<div class="column">
<h2 class="page-sub-title">
{{headerTitle}}
{{ headerTitle }}
</h2>
<p class="small-12 column" v-html="headerContent">
</p>
<p class="small-12 column" v-html="headerContent"></p>
</div>
</template>

View file

@ -1,8 +1,6 @@
<template>
<div class="wizard-body columns content-box small-9">
<loading-state v-if="showLoader" :message="emptyStateMessage">
</loading-state>
<form v-if="!showLoader" class="row" @submit.prevent="addAgents()">
<form class="row" @submit.prevent="addAgents()">
<div class="medium-12 columns">
<page-header
:header-title="$t('INBOX_MGMT.ADD.AGENTS.TITLE')"
@ -31,8 +29,11 @@
</span>
</label>
</div>
<div class="medium-12 columns text-right">
<input type="submit" value="Create Inbox" class="button" />
<div class="medium-12 columns">
<woot-submit-button
:button-text="$t('INBOX_MGMT.AGENTS.BUTTON_TEXT')"
:loading="isCreating"
/>
</div>
</div>
</form>
@ -44,15 +45,13 @@
/* global bus */
import { mapGetters } from 'vuex';
import ChannelApi from '../../../../api/channels';
import InboxMembersAPI from '../../../../api/inboxMembers';
import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader';
import LoadingState from '../../../../components/widgets/LoadingState';
export default {
components: {
PageHeader,
LoadingState,
},
validations: {
@ -65,9 +64,8 @@ export default {
data() {
return {
emptyStateMessage: this.$t('INBOX_MGMT.AGENTS.ADD_AGENTS'),
showLoader: false,
selectedAgents: [],
isCreating: false,
};
},
@ -82,25 +80,24 @@ export default {
},
methods: {
addAgents() {
async addAgents() {
this.isCreating = true;
const inboxId = this.$route.params.inbox_id;
ChannelApi.addAgentsToChannel(inboxId, this.selectedAgents.map(x => x.id))
.then(() => {
this.isCreating = false;
router.replace({
name: 'settings_inbox_finish',
params: {
page: 'new',
inbox_id: this.$route.params.inbox_id,
website_token: this.$route.params.website_token,
},
});
})
.catch(error => {
bus.$emit('newToastMessage', error.message);
this.isCreating = false;
const selectedAgents = this.selectedAgents.map(x => x.id);
try {
await InboxMembersAPI.create({ inboxId, agentList: selectedAgents });
router.replace({
name: 'settings_inbox_finish',
params: {
page: 'new',
inbox_id: this.$route.params.inbox_id,
},
});
} catch (error) {
bus.$emit('newToastMessage', error.message);
}
this.isCreating = false;
},
},
};

View file

@ -7,7 +7,7 @@
>
<div class="medium-12 columns text-center">
<div class="website--code">
<woot-code v-if="$route.params.website_token" :script="websiteScript">
<woot-code v-if="currentInbox.website_token" :script="websiteScript">
</woot-code>
</div>
<router-link
@ -33,6 +33,11 @@ export default {
EmptyState,
},
computed: {
currentInbox() {
return this.$store.getters['inboxes/getInbox'](
this.$route.params.inbox_id
);
},
message() {
if (!this.$route.params.website_token) {
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
@ -40,7 +45,7 @@ export default {
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
},
websiteScript() {
return createWebsiteWidgetScript(this.$route.params.website_token);
return createWebsiteWidgetScript(this.currentInbox.website_token);
},
},
};

View file

@ -18,9 +18,9 @@
<tr v-for="item in inboxesList" :key="item.id">
<td>
<img
v-if="item.avatarUrl"
v-if="item.avatar_url"
class="woot-thumbnail"
:src="item.avatarUrl"
:src="item.avatar_url"
alt="No Page Image"
/>
<img
@ -32,11 +32,11 @@
</td>
<!-- Short Code -->
<td>
<span class="agent-name">{{ item.label }}</span>
<span v-if="item.channelType === 'Channel::FacebookPage'">
<span class="agent-name">{{ item.name }}</span>
<span v-if="item.channel_type === 'Channel::FacebookPage'">
Facebook
</span>
<span v-if="item.channelType === 'Channel::WebWidget'">
<span v-if="item.channel_type === 'Channel::WebWidget'">
Website
</span>
</td>
@ -44,28 +44,23 @@
<!-- Action Buttons -->
<td>
<div class="button-wrapper">
<div v-if="isAdmin()" @click="openSettings(item)">
<router-link :to="`/app/settings/inboxes/${item.id}`">
<woot-submit-button
v-if="isAdmin()"
:button-text="$t('INBOX_MGMT.SETTINGS')"
icon-class="ion-gear-b"
button-class="link hollow grey-btn"
/>
</div>
<!-- <div>
<woot-submit-button
:button-text="$t('INBOX_MGMT.REAUTH')"
icon-class="ion-edit"
</router-link>
<woot-submit-button
v-if="isAdmin()"
:button-text="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[item.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn"
/>
</div> -->
<div v-if="isAdmin()" @click="openDelete(item)">
<woot-submit-button
:button-text="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[item.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn"
/>
</div>
@click="openDelete(item)"
/>
</div>
</td>
</tr>
@ -120,22 +115,22 @@ export default {
},
computed: {
...mapGetters({
inboxesList: 'getInboxesList',
inboxesList: 'inboxes/getInboxes',
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedInbox.label
this.selectedInbox.name
}`;
},
deleteRejectText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedInbox.label
this.selectedInbox.name
}`;
},
deleteMessage() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedInbox.label
this.selectedInbox.name
} ?`;
},
},
@ -148,21 +143,19 @@ export default {
this.showSettings = false;
this.selectedInbox = {};
},
deleteInbox({ channel_id }) {
this.$store
.dispatch('deleteInbox', channel_id)
.then(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
)
)
.catch(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
)
async deleteInbox({ id }) {
try {
await this.$store.dispatch('inboxes/delete', id);
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
);
} catch (error) {
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
);
}
},
confirmDeletion() {

View file

@ -1,148 +1,170 @@
<template>
<woot-modal class-name="settings-modal" :show.sync="show" :on-close="onClose">
<div class="settings">
<woot-modal-header
:header-image="inbox.avatarUrl"
:header-title="inbox.label"
/>
<div
v-if="inbox.channelType === 'Channel::FacebookPage'"
class="settings--content"
<div class="settings columns container">
<woot-modal-header
:header-image="inbox.avatarUrl"
:header-title="inbox.name"
/>
<div
v-if="inbox.channel_type === 'Channel::FacebookPage'"
class="settings--content"
>
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
>
</settings-form-header>
<woot-code :script="messengerScript"></woot-code>
</div>
<div v-else-if="inbox.channel_type === 'Channel::WebWidget'">
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
>
</settings-form-header>
<woot-code :script="messengerScript"></woot-code>
</div>
<div v-else-if="inbox.channelType === 'Channel::WebWidget'">
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
>
</settings-form-header>
<woot-code :script="webWidgetScript"></woot-code>
</div>
<!-- <div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="isUpdating"
v-on:update="updateAgents"
>
</settings-form-header>
<Compact v-model="widgetColor" />
</div> -->
<woot-code :script="webWidgetScript"></woot-code>
</div>
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
:title="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL')"
:sub-title="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.PLACEHOLDER')
"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="isUpdating"
@update="updateAgents"
:is-updating="uiFlags.isUpdating"
@update="updateWidgetColor"
>
</settings-form-header>
<multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
placeholder="Pick some"
@select="$v.selectedAgents.$touch"
/>
<Compact v-model="inbox.widget_color" class="widget-color--selector" />
</div>
</div>
</woot-modal>
<div class="settings--content">
<settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="isAgentListUpdating"
@update="updateAgents"
>
</settings-form-header>
<multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
placeholder="Pick some"
@select="$v.selectedAgents.$touch"
/>
</div>
</div>
</template>
<script>
/* eslint no-console: 0 */
/* eslint-disable no-useless-escape */
/* global bus */
import { mapGetters } from 'vuex';
import {
createWebsiteWidgetScript,
createMessengerScript,
} from 'dashboard/helper/scriptGenerator';
import { Compact } from 'vue-color';
import SettingsFormHeader from '../../../../components/SettingsFormHeader.vue';
export default {
components: {
Compact,
SettingsFormHeader,
},
props: ['onClose', 'inbox', 'show'],
data() {
return {
selectedAgents: [],
isUpdating: false,
widgetColor: { hex: this.inbox.widgetColor },
isAgentListUpdating: false,
};
},
computed: {
...mapGetters({
agentList: 'agents/getAgents',
uiFlags: 'inboxes/getUIFlags',
}),
currentInboxId() {
return this.$route.params.inboxId;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
},
webWidgetScript() {
return createWebsiteWidgetScript(this.inbox.websiteToken);
return createWebsiteWidgetScript(this.inbox.website_token);
},
messengerScript() {
return createMessengerScript(this.inbox.pageId);
return createMessengerScript(this.inbox.page_id);
},
},
mounted() {
this.$store.dispatch('agents/get').then(() => {
this.$store.dispatch('agents/get');
this.$store.dispatch('inboxes/get').then(() => {
this.fetchAttachedAgents();
});
},
methods: {
fetchAttachedAgents() {
this.$store
.dispatch('listInboxAgents', {
inboxId: this.inbox.channel_id,
})
.then(response => {
const { payload } = response.data;
payload.forEach(el => {
const [item] = this.agentList.filter(
agent => agent.id === el.user_id
);
if (item) this.selectedAgents.push(item);
});
})
.catch(error => {
console.log(error);
});
showAlert(message) {
bus.$emit('newToastMessage', message);
},
updateAgents() {
const agentList = this.selectedAgents.map(el => el.id);
this.isUpdating = true;
this.$store
.dispatch('updateInboxAgents', {
inboxId: this.inbox.channel_id,
agentList,
})
.then(() => {
this.isUpdating = false;
bus.$emit(
'newToastMessage',
this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE')
);
})
.catch(() => {
this.isUpdating = false;
bus.$emit(
'newToastMessage',
this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE')
);
async fetchAttachedAgents() {
try {
const response = await this.$store.dispatch('inboxMembers/get', {
inboxId: this.currentInboxId,
});
const {
data: { payload },
} = response;
payload.forEach(el => {
const [item] = this.agentList.filter(
agent => agent.id === el.user_id
);
if (item) {
this.selectedAgents.push(item);
}
});
} catch (error) {
console.log(error);
}
},
async updateAgents() {
const agentList = this.selectedAgents.map(el => el.id);
this.isAgentListUpdating = true;
try {
await this.$store.dispatch('inboxMembers/create', {
inboxId: this.currentInboxId,
agentList,
});
this.showAlert(this.$t('AGENT_MGMT.EDIT.API.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(this.$t('AGENT_MGMT.EDIT.API.ERROR_MESSAGE'));
}
this.isAgentListUpdating = false;
},
async updateWidgetColor() {
try {
await this.$store.dispatch('inboxes/updateWebsiteChannel', {
id: this.inbox.channel_id,
website: {
widget_color: this.getWidgetColor(this.inbox.widget_color),
},
});
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
}
},
getWidgetColor() {
return typeof this.inbox.widget_color !== 'object'
? this.inbox.widget_color
: this.inbox.widget_color.hex;
},
},
validations: {

View file

@ -219,14 +219,11 @@ export default {
this.emptyStateMessage = this.$t('INBOX_MGMT.DETAILS.CREATING_CHANNEL');
this.isCreating = true;
this.$store
.dispatch('addInboxItem', {
channel: this.channel,
params: this.channelParams(),
})
.then(response => {
.dispatch('inboxes/createFBChannel', this.channelParams())
.then(data => {
router.replace({
name: 'settings_inboxes_add_agents',
params: { page: 'new', inbox_id: response.data.id },
params: { page: 'new', inbox_id: data.id },
});
})
.catch(() => {

View file

@ -55,8 +55,8 @@
</template>
<script>
/* global bus */
import { Compact } from 'vue-color';
import { mapGetters } from 'vuex';
import router from '../../../../index';
import PageHeader from '../../SettingsSubPageHeader';
@ -73,26 +73,28 @@ export default {
isCreating: false,
};
},
mounted() {
bus.$on('new_website_channel', ({ inboxId, websiteToken }) => {
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
}),
},
methods: {
async createChannel() {
const website = await this.$store.dispatch(
'inboxes/createWebsiteChannel',
{
website: {
website_name: this.websiteName,
website_url: this.websiteUrl,
widget_color: this.widgetColor.hex,
},
}
);
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: inboxId,
website_token: websiteToken,
},
});
});
},
methods: {
createChannel() {
this.isCreating = true;
this.$store.dispatch('addWebsiteChannel', {
website: {
website_name: this.websiteName,
website_url: this.websiteUrl,
widget_color: this.widgetColor.hex,
inbox_id: website.id,
},
});
},

View file

@ -1,5 +1,6 @@
/* eslint arrow-body-style: 0 */
import SettingsContent from '../Wrapper';
import Settings from './Settings';
import InboxHome from './Index';
import InboxChannel from './InboxChannels';
import ChannelList from './ChannelList';
@ -64,6 +65,12 @@ export default {
},
],
},
{
path: ':inboxId',
name: 'settings_inbox_show',
component: Settings,
roles: ['administrator'],
},
],
},
],

View file

@ -7,8 +7,9 @@ import billing from './modules/billing';
import cannedResponse from './modules/cannedResponse';
import Channel from './modules/channels';
import conversations from './modules/conversations';
import inboxes from './modules/inboxes';
import inboxMembers from './modules/inboxMembers';
import reports from './modules/reports';
import sideMenuItems from './modules/sidebar';
Vue.use(Vuex);
export default new Vuex.Store({
@ -19,7 +20,8 @@ export default new Vuex.Store({
cannedResponse,
Channel,
conversations,
inboxes,
inboxMembers,
reports,
sideMenuItems,
},
});

View file

@ -0,0 +1,24 @@
import InboxMembersAPI from '../../api/inboxMembers';
const state = {};
const getters = {};
const actions = {
get(_, { inboxId }) {
return InboxMembersAPI.show(inboxId);
},
create(_, { inboxId, agentList }) {
return InboxMembersAPI.create({ inboxId, agentList });
},
};
const mutations = {};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};

View file

@ -0,0 +1,109 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import * as types from '../mutation-types';
import InboxesAPI from '../../api/inboxes';
import WebChannel from '../../api/channel/webChannel';
import FBChannel from '../../api/channel/fbChannel';
export const state = {
records: [],
uiFlags: {
isFetching: false,
isFetchingItem: false,
isCreating: false,
isUpdating: false,
isDeleting: false,
},
};
export const getters = {
getInboxes($state) {
return $state.records;
},
getInbox: $state => inboxId => {
const [inbox] = $state.records.filter(
record => record.id === Number(inboxId)
);
return inbox || {};
},
getUIFlags($state) {
return $state.uiFlags;
},
};
export const actions = {
get: async ({ commit }) => {
commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: true });
try {
const response = await InboxesAPI.get();
commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: false });
commit(types.default.SET_INBOXES, response.data.payload);
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: false });
}
},
createWebsiteChannel: async ({ commit }, params) => {
try {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
const response = await WebChannel.create(params);
commit(types.default.ADD_INBOXES, response.data);
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
return response.data;
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
throw new Error(error);
}
},
createFBChannel: async ({ commit }, params) => {
try {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
const response = await FBChannel.create(params);
commit(types.default.ADD_INBOXES, response.data);
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
return response.data;
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
throw new Error(error);
}
},
updateWebsiteChannel: async ({ commit }, { id, ...inboxParams }) => {
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: true });
try {
const response = await WebChannel.update(id, inboxParams);
commit(types.default.EDIT_INBOXES, response.data);
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: false });
throw new Error(error);
}
},
delete: async ({ commit }, inboxId) => {
commit(types.default.SET_INBOXES_UI_FLAG, { isDeleting: true });
try {
await InboxesAPI.delete(inboxId);
commit(types.default.DELETE_INBOXES, inboxId);
commit(types.default.SET_INBOXES_UI_FLAG, { isDeleting: false });
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isDeleting: false });
throw new Error(error);
}
},
};
export const mutations = {
[types.default.SET_INBOXES_UI_FLAG]($state, uiFlag) {
$state.uiFlags = { ...$state.uiFlags, ...uiFlag };
},
[types.default.SET_INBOXES]: MutationHelpers.set,
[types.default.SET_INBOXES_ITEM]: MutationHelpers.setSingleRecord,
[types.default.ADD_INBOXES]: MutationHelpers.create,
[types.default.EDIT_INBOXES]: MutationHelpers.update,
[types.default.DELETE_INBOXES]: MutationHelpers.destroy,
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};

View file

@ -1,195 +0,0 @@
/* eslint no-console: 0 */
/* eslint-env browser */
/* eslint no-param-reassign: 0 */
/* global bus */
// import * as types from '../mutation-types';
import defaultState from '../../i18n/default-sidebar';
import * as types from '../mutation-types';
import Account from '../../api/account';
import ChannelApi from '../../api/channels';
import { frontendURL } from '../../helper/URLHelper';
import WebChannel from '../../api/channel/webChannel';
const state = defaultState;
// inboxes fetch flag
state.inboxesLoading = false;
const getters = {
getMenuItems(_state) {
return _state.menuGroup;
},
getInboxesList(_state) {
return _state.menuGroup.common.menuItems.inbox.children;
},
getInboxLoadingStatus(_state) {
return _state.inboxesLoading;
},
};
const actions = {
// Fetch Labels
fetchLabels({ commit }) {
Account.getLabels()
.then(response => {
commit(types.default.SET_LABELS, response.data);
})
.catch();
},
// Fetch Inboxes
fetchInboxes({ commit }) {
commit(types.default.INBOXES_LOADING, true);
return new Promise((resolve, reject) => {
Account.getInboxes()
.then(response => {
commit(types.default.INBOXES_LOADING, false);
commit(types.default.SET_INBOXES, response.data);
resolve();
})
.catch(error => {
commit(types.default.INBOXES_LOADING, false);
reject(error);
});
});
},
deleteInbox({ commit }, id) {
return new Promise((resolve, reject) => {
Account.deleteInbox(id)
.then(response => {
if (response.status === 200) {
commit(types.default.DELETE_INBOX, id);
resolve();
} else {
reject();
}
})
.catch(error => {
reject(error);
});
});
},
addWebsiteChannel: async ({ commit }, params) => {
try {
const response = await WebChannel.create(params);
commit(types.default.SET_INBOX_ITEM, response);
bus.$emit('new_website_channel', {
inboxId: response.data.id,
websiteToken: response.data.website_token,
});
} catch (error) {
// Handle error
}
},
addInboxItem({ commit }, { channel, params }) {
const donePromise = new Promise(resolve => {
ChannelApi.createChannel(channel, params)
.then(response => {
commit(types.default.SET_INBOX_ITEM, response);
resolve(response);
})
.catch(error => {
console.log(error);
});
});
return donePromise;
},
listInboxAgents(_, { inboxId }) {
return new Promise((resolve, reject) => {
Account.listInboxAgents(inboxId)
.then(response => {
if (response.status === 200) {
resolve(response.data);
} else {
reject();
}
})
.catch(error => {
reject(error);
});
});
},
updateInboxAgents(_, { inboxId, agentList }) {
return new Promise((resolve, reject) => {
Account.updateInboxAgents(inboxId, agentList)
.then(response => {
if (response.status === 200) {
resolve(response.data);
} else {
reject();
}
})
.catch(error => {
reject(error);
});
});
},
};
const mutations = {
// Set Labels
[types.default.SET_LABELS](_state, data) {
let payload = data.data.payload.labels;
payload = payload.map(item => ({
label: item,
toState: `/#/${item}`,
}));
// Identify menuItem to update
// May have more than one object to update
// Iterate it accordingly. Updating commmon sidebar now.
const { menuItems } = _state.menuGroup.common;
// Update children for key `label`
menuItems.labels.children = payload;
},
[types.default.INBOXES_LOADING](_state, flag) {
_state.inboxesLoading = flag;
},
// Set Inboxes
[types.default.SET_INBOXES](_state, data) {
let { payload } = data.data;
payload = payload.map(item => ({
channel_id: item.id,
label: item.name,
toState: frontendURL(`inbox/${item.id}`),
channelType: item.channel_type,
avatarUrl: item.avatar_url,
pageId: item.page_id,
websiteToken: item.website_token,
widgetColor: item.widget_color,
}));
// Identify menuItem to update
// May have more than one object to update
// Iterate it accordingly. Updating commmon sidebar now.
const { menuItems } = _state.menuGroup.common;
// Update children for key `inbox`
menuItems.inbox.children = payload;
},
[types.default.SET_INBOX_ITEM](_state, { data }) {
const { menuItems } = _state.menuGroup.common;
// Update children for key `inbox`
menuItems.inbox.children.push({
channel_id: data.id,
label: data.name,
toState: frontendURL(`inbox/${data.id}`),
channelType: data.channel_type,
avatarUrl: data.avatar_url === undefined ? null : data.avatar_url,
pageId: data.page_id,
websiteToken: data.website_token,
widgetColor: data.widget_color,
});
},
[types.default.DELETE_INBOX](_state, id) {
const { menuItems } = _state.menuGroup.common;
let inboxList = menuItems.inbox.children;
inboxList = inboxList.filter(inbox => inbox.channel_id !== id);
menuItems.inbox.children = inboxList;
},
};
export default {
state,
getters,
actions,
mutations,
};

View file

@ -0,0 +1,116 @@
import axios from 'axios';
import { actions } from '../../inboxes';
import * as types from '../../../mutation-types';
import inboxList from './fixtures';
const commit = jest.fn();
global.axios = axios;
jest.mock('axios');
describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: { payload: inboxList } });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isFetching: true }],
[types.default.SET_INBOXES_UI_FLAG, { isFetching: false }],
[types.default.SET_INBOXES, inboxList],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isFetching: true }],
[types.default.SET_INBOXES_UI_FLAG, { isFetching: false }],
]);
});
});
describe('#createWebsiteChannel', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: inboxList[0] });
await actions.createWebsiteChannel({ commit }, inboxList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isCreating: true }],
[types.default.ADD_INBOXES, inboxList[0]],
[types.default.SET_INBOXES_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.createWebsiteChannel({ commit })).rejects.toThrow(
Error
);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isCreating: true }],
[types.default.SET_INBOXES_UI_FLAG, { isCreating: false }],
]);
});
});
describe('#createFBChannel', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: inboxList[0] });
await actions.createFBChannel({ commit }, inboxList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isCreating: true }],
[types.default.ADD_INBOXES, inboxList[0]],
[types.default.SET_INBOXES_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.createFBChannel({ commit })).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isCreating: true }],
[types.default.SET_INBOXES_UI_FLAG, { isCreating: false }],
]);
});
});
describe('#updateWebsiteChannel', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: inboxList[0] });
await actions.updateWebsiteChannel({ commit }, inboxList[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: true }],
[types.default.EDIT_INBOXES, inboxList[0]],
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.updateWebsiteChannel({ commit }, inboxList[0])
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: true }],
[types.default.SET_INBOXES_UI_FLAG, { isUpdating: false }],
]);
});
});
describe('#delete', () => {
it('sends correct actions if API is success', async () => {
axios.delete.mockResolvedValue({ data: inboxList[0] });
await actions.delete({ commit }, inboxList[0].id);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isDeleting: true }],
[types.default.DELETE_INBOXES, inboxList[0].id],
[types.default.SET_INBOXES_UI_FLAG, { isDeleting: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.delete({ commit }, inboxList[0].id)).rejects.toThrow(
Error
);
expect(commit.mock.calls).toEqual([
[types.default.SET_INBOXES_UI_FLAG, { isDeleting: true }],
[types.default.SET_INBOXES_UI_FLAG, { isDeleting: false }],
]);
});
});
});

View file

@ -0,0 +1,42 @@
export default [
{
id: 1,
channel_id: 1,
name: 'Test FacebookPage 1',
channel_type: 'Channel::FacebookPage',
avatar_url: 'random_image.png',
page_id: '12345',
widget_color: null,
website_token: null,
},
{
id: 2,
channel_id: 2,
name: 'Test Widget 1',
channel_type: 'Channel::WebWidget',
avatar_url: null,
page_id: null,
widget_color: '#7B64FF',
website_token: 'randomid123',
},
{
id: 3,
channel_id: 3,
name: 'Test Widget 2',
channel_type: 'Channel::WebWidget',
avatar_url: null,
page_id: null,
widget_color: '#68BC00',
website_token: 'randomid124',
},
{
id: 4,
channel_id: 4,
name: 'Test Widget 3',
channel_type: 'Channel::WebWidget',
avatar_url: null,
page_id: null,
widget_color: '#68BC00',
website_token: 'randomid125',
},
];

View file

@ -0,0 +1,46 @@
import { getters } from '../../inboxes';
import inboxList from './fixtures';
describe('#getters', () => {
it('getInboxes', () => {
const state = {
records: inboxList,
};
expect(getters.getInboxes(state)).toEqual(inboxList);
});
it('getInbox', () => {
const state = {
records: inboxList,
};
expect(getters.getInbox(state)(1)).toEqual({
id: 1,
channel_id: 1,
name: 'Test FacebookPage 1',
channel_type: 'Channel::FacebookPage',
avatar_url: 'random_image.png',
page_id: '12345',
widget_color: null,
website_token: null,
});
});
it('getUIFlags', () => {
const state = {
uiFlags: {
isFetching: true,
isFetchingItem: false,
isCreating: false,
isUpdating: false,
isDeleting: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
isFetching: true,
isFetchingItem: false,
isCreating: false,
isUpdating: false,
isDeleting: false,
});
});
});

View file

@ -0,0 +1,94 @@
import * as types from '../../../mutation-types';
import { mutations } from '../../inboxes';
import inboxList from './fixtures';
describe('#mutations', () => {
describe('#SET_INBOXES', () => {
it('set inbox records', () => {
const state = { records: [] };
mutations[types.default.SET_INBOXES](state, inboxList);
expect(state.records).toEqual(inboxList);
});
});
describe('#SET_INBOXES_ITEM', () => {
it('push inbox if inbox doesnot exist to the store', () => {
const state = {
records: [],
};
mutations[types.default.SET_INBOXES_ITEM](state, inboxList[0]);
expect(state.records).toEqual([inboxList[0]]);
});
it('update inbox if it exists to the store', () => {
const state = {
records: [
{
id: 1,
channel_id: 1,
name: 'Test FacebookPage',
channel_type: 'Channel::FacebookPage',
avatar_url: 'random_image1.png',
page_id: '1235',
widget_color: null,
website_token: null,
},
],
};
mutations[types.default.SET_INBOXES_ITEM](state, inboxList[0]);
expect(state.records).toEqual([inboxList[0]]);
});
});
describe('#ADD_INBOXES', () => {
it('push new record in the inbox store', () => {
const state = {
records: [],
};
mutations[types.default.ADD_INBOXES](state, inboxList[0]);
expect(state.records).toEqual([inboxList[0]]);
});
});
describe('#EDIT_INBOXES', () => {
it('update inbox in the store', () => {
const state = {
records: [
{
id: 1,
channel_id: 1,
name: 'Test FacebookPage',
channel_type: 'Channel::FacebookPage',
avatar_url: 'random_image1.png',
page_id: '1235',
widget_color: null,
website_token: null,
},
],
};
mutations[types.default.EDIT_INBOXES](state, inboxList[0]);
expect(state.records).toEqual([inboxList[0]]);
});
});
describe('#DELETE_INBOXES', () => {
it('delete inbox from store', () => {
const state = {
records: [inboxList[0]],
};
mutations[types.default.DELETE_INBOXES](state, 1);
expect(state.records).toEqual([]);
});
});
describe('#DELETE_INBOXES', () => {
it('delete inbox from store', () => {
const state = {
uiFlags: { isFetchingItem: false },
};
mutations[types.default.SET_INBOXES_UI_FLAG](state, {
isFetchingItem: true,
});
expect(state.uiFlags).toEqual({ isFetchingItem: true });
});
});
});

View file

@ -31,14 +31,13 @@ export default {
SET_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
// labels
SET_LABELS: 'SET_LABELS',
// Set Inboxes
INBOXES_LOADING: 'INBOXES_LOADING',
// Inboxes
SET_INBOXES_UI_FLAG: 'SET_INBOXES_UI_FLAG',
SET_INBOXES: 'SET_INBOXES',
SET_INBOX_ITEM: 'SET_INBOX_ITEM',
DELETE_INBOX: 'DELETE_INBOX',
ADD_INBOXES: 'ADD_INBOXES',
EDIT_INBOXES: 'EDIT_INBOXES',
DELETE_INBOXES: 'DELETE_INBOXES',
// Agent
SET_AGENT_FETCHING_STATUS: 'SET_AGENT_FETCHING_STATUS',

View file

@ -6,6 +6,15 @@ export const create = (state, data) => {
state.records.push(data);
};
export const setSingleRecord = (state, data) => {
const recordIndex = state.records.findIndex(record => record.id === data.id);
if (recordIndex > -1) {
state.records[recordIndex] = data;
} else {
create(state, data);
}
};
export const update = (state, data) => {
state.records.forEach((element, index) => {
if (element.id === data.id) {

View file

@ -39,6 +39,12 @@ $input-height: $space-two * 2;
padding: $space-small $space-slab;
}
&.default {
font-size: $font-size-default;
height: $space-medium;
padding: $space-smaller $space-slab;
}
&.large {
font-size: $font-size-medium;
height: $space-larger;

View file

@ -22,6 +22,7 @@ module Channel
validates :website_name, presence: true
validates :website_url, presence: true
validates :widget_color, presence: true
belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy

View file

@ -24,6 +24,10 @@ class InboxPolicy < ApplicationPolicy
@user.administrator?
end
def update?
@user.administrator?
end
def destroy?
@user.administrator?
end

View file

@ -1,8 +1,6 @@
json.data do
json.payload do
json.array! @agents do |agent|
json.user_id agent.id
json.name agent.name
end
json.payload do
json.array! @agents do |agent|
json.user_id agent.id
json.name agent.name
end
end

View file

@ -1,17 +1,12 @@
json.data do
json.meta do
end
json.payload do
json.array! @inboxes do |inbox|
json.id inbox.id
json.channel_id inbox.channel_id
json.name inbox.name
json.channel_type inbox.channel_type
json.avatar_url inbox.channel.try(:avatar).try(:url)
json.page_id inbox.channel.try(:page_id)
json.widget_color inbox.channel.try(:widget_color)
json.website_token inbox.channel.try(:website_token)
end
json.payload do
json.array! @inboxes do |inbox|
json.id inbox.id
json.channel_id inbox.channel_id
json.name inbox.name
json.channel_type inbox.channel_type
json.avatar_url inbox.channel.try(:avatar).try(:url)
json.page_id inbox.channel.try(:page_id)
json.widget_color inbox.channel.try(:widget_color)
json.website_token inbox.channel.try(:website_token)
end
end

View file

@ -3,3 +3,4 @@ json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color)

View file

@ -0,0 +1,6 @@
json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.website_token @inbox.channel.website_token
json.widget_color @inbox.channel.widget_color

View file

@ -26,7 +26,7 @@ Rails.application.routes.draw do
namespace :widget do
resources :messages, only: [:index, :create]
resources :inboxes, only: [:create]
resources :inboxes, only: [:create, :update]
end
namespace :actions do
@ -92,7 +92,7 @@ Rails.application.routes.draw do
# Sidekiq Web UI
require 'sidekiq/web'
authenticate :user, lambda { |u| u.administrator? } do
authenticate :user, ->(u) { u.administrator? } do
mount Sidekiq::Web => '/sidekiq'
end

View file

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

View file

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

View file

@ -3,7 +3,7 @@
FactoryBot.define do
factory :inbox do
account
association :channel, factory: :channel_widget
name { 'Inbox' }
channel { FactoryBot.build(:channel_widget, account: account) }
end
end