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 enabled: true
brakeman: brakeman:
enabled: true enabled: true
checks:
similar-code:
enabled: false
exclude_patterns: exclude_patterns:
- "spec/" - "spec/"
- "**/specs/" - "**/specs/"

View file

@ -1,17 +1,25 @@
class Api::V1::Widget::InboxesController < Api::BaseController class Api::V1::Widget::InboxesController < Api::BaseController
before_action :authorize_request before_action :authorize_request
before_action :set_web_widget_channel, only: [:update]
before_action :set_inbox, only: [:update]
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
channel = web_widgets.create!( channel = web_widgets.create!(
website_name: permitted_params[:website_name], website_name: permitted_params[:website][:website_name],
website_url: permitted_params[:website_url], website_url: permitted_params[:website][:website_url],
widget_color: permitted_params[:widget_color] 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
end end
def update
@channel.update!(
widget_color: permitted_params[:website][:widget_color]
)
end
private private
def authorize_request def authorize_request
@ -26,7 +34,15 @@ class Api::V1::Widget::InboxesController < Api::BaseController
current_account.web_widgets current_account.web_widgets
end 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 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
end end

View file

@ -4,7 +4,8 @@ const API_VERSION = `/api/v1`;
class ApiClient { class ApiClient {
constructor(url) { constructor(url) {
this.url = `${API_VERSION}/${url}`; this.apiVersion = API_VERSION;
this.url = `${this.apiVersion}/${url}`;
} }
get() { 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, contact_id: contactId,
}); });
} }
create(params) {
return axios.post(
`${this.apiVersion}/callbacks/register_facebook_page`,
params
);
}
} }
export default new FBChannel(); export default new FBChannel();

View file

@ -5,19 +5,6 @@
import endPoints from './endPoints'; import endPoints from './endPoints';
export default { 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) { fetchFacebookPages(token) {
const urlData = endPoints('fetchFacebookPages'); const urlData = endPoints('fetchFacebookPages');
urlData.params.omniauth_token = token; urlData.params.omniauth_token = token;

View file

@ -24,26 +24,6 @@ const endPoints = {
params: { inbox_id: null }, 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: { fetchFacebookPages: {
url: 'api/v1/callbacks/get_facebook_pages.json', url: 'api/v1/callbacks/get_facebook_pages.json',
params: { omniauth_token: '' }, 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 => { 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 // Conversation header - Light BG
.settings-header { .settings-header {
@include padding($space-small $space-normal); @include padding($space-small $space-normal);
@ -196,51 +203,23 @@
} }
} }
.settings-modal { .settings--content {
height: 80%; @include margin($space-small $space-medium);
max-width: 1040px;
width: 100%;
.delete-wrapper { .title {
position: absolute; font-weight: $font-weight-medium;
bottom: 0;
width: 100%;
@include flex;
flex-direction: row;
justify-content: space-between;
@include padding($space-normal $space-large);
a {
margin-left: $space-normal;
}
} }
.settings--content { .code {
@include margin($space-medium); max-height: $space-mega;
overflow: scroll;
white-space: nowrap;
@include padding($space-one);
background: $color-background;
.title { code {
font-weight: $font-weight-medium; background: transparent;
} border: 0;
.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;
} }
} }
} }

View file

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

View file

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

View file

@ -1,11 +1,11 @@
<template> <template>
<div class="column page-top-bar"> <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"> <h2 class="page-sub-title">
{{headerTitle}} {{ headerTitle }}
</h2> </h2>
<p class="small-12 column"> <p v-if="headerContent" class="small-12 column">
{{headerContent}} {{ headerContent }}
</p> </p>
</div> </div>
</template> </template>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,8 @@
"PLACEHOLDER": "Enter your website domain (eg: acme.com)" "PLACEHOLDER": "Enter your website domain (eg: acme.com)"
}, },
"WIDGET_COLOR": { "WIDGET_COLOR": {
"LABEL": "Widget Color" "LABEL": "Widget Color",
"PLACEHOLDER": "Update the widget color used in widget"
}, },
"SUBMIT_BUTTON":"Create inbox" "SUBMIT_BUTTON":"Create inbox"
}, },
@ -52,10 +53,11 @@
"LOADING_FB": "Authenticating you with Facebook...", "LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...", "ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"CREATING_CHANNEL": "Creating your Inbox...", "CREATING_CHANNEL": "Creating your Inbox...",
"TITLE": "Configure Inbox Deatails", "TITLE": "Configure Inbox Details",
"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." "DESC": ""
}, },
"AGENTS": { "AGENTS": {
"BUTTON_TEXT": "Add agents",
"ADD_AGENTS": "Adding Agents to your Inbox..." "ADD_AGENTS": "Adding Agents to your Inbox..."
}, },
"FINISH": { "FINISH": {
@ -66,6 +68,12 @@
}, },
"REAUTH": "Reauthorize", "REAUTH": "Reauthorize",
"VIEW": "View", "VIEW": "View",
"EDIT": {
"API": {
"SUCCESS_MESSAGE": "Widget color updated successfully",
"ERROR_MESSAGE": "Could not update widget color. Please try again later."
}
},
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Delete", "BUTTON_TEXT": "Delete",
"CONFIRM": { "CONFIRM": {

View file

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

View file

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

View file

@ -1,8 +1,6 @@
<template> <template>
<div class="wizard-body columns content-box small-9"> <div class="wizard-body columns content-box small-9">
<loading-state v-if="showLoader" :message="emptyStateMessage"> <form class="row" @submit.prevent="addAgents()">
</loading-state>
<form v-if="!showLoader" class="row" @submit.prevent="addAgents()">
<div class="medium-12 columns"> <div class="medium-12 columns">
<page-header <page-header
:header-title="$t('INBOX_MGMT.ADD.AGENTS.TITLE')" :header-title="$t('INBOX_MGMT.ADD.AGENTS.TITLE')"
@ -31,8 +29,11 @@
</span> </span>
</label> </label>
</div> </div>
<div class="medium-12 columns text-right"> <div class="medium-12 columns">
<input type="submit" value="Create Inbox" class="button" /> <woot-submit-button
:button-text="$t('INBOX_MGMT.AGENTS.BUTTON_TEXT')"
:loading="isCreating"
/>
</div> </div>
</div> </div>
</form> </form>
@ -44,15 +45,13 @@
/* global bus */ /* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ChannelApi from '../../../../api/channels'; import InboxMembersAPI from '../../../../api/inboxMembers';
import router from '../../../index'; import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader'; import PageHeader from '../SettingsSubPageHeader';
import LoadingState from '../../../../components/widgets/LoadingState';
export default { export default {
components: { components: {
PageHeader, PageHeader,
LoadingState,
}, },
validations: { validations: {
@ -65,9 +64,8 @@ export default {
data() { data() {
return { return {
emptyStateMessage: this.$t('INBOX_MGMT.AGENTS.ADD_AGENTS'),
showLoader: false,
selectedAgents: [], selectedAgents: [],
isCreating: false,
}; };
}, },
@ -82,25 +80,24 @@ export default {
}, },
methods: { methods: {
addAgents() { async addAgents() {
this.isCreating = true; this.isCreating = true;
const inboxId = this.$route.params.inbox_id; const inboxId = this.$route.params.inbox_id;
ChannelApi.addAgentsToChannel(inboxId, this.selectedAgents.map(x => x.id)) const selectedAgents = this.selectedAgents.map(x => x.id);
.then(() => {
this.isCreating = false; try {
router.replace({ await InboxMembersAPI.create({ inboxId, agentList: selectedAgents });
name: 'settings_inbox_finish', router.replace({
params: { name: 'settings_inbox_finish',
page: 'new', params: {
inbox_id: this.$route.params.inbox_id, page: 'new',
website_token: this.$route.params.website_token, inbox_id: this.$route.params.inbox_id,
}, },
});
})
.catch(error => {
bus.$emit('newToastMessage', error.message);
this.isCreating = false;
}); });
} 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="medium-12 columns text-center">
<div class="website--code"> <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> </woot-code>
</div> </div>
<router-link <router-link
@ -33,6 +33,11 @@ export default {
EmptyState, EmptyState,
}, },
computed: { computed: {
currentInbox() {
return this.$store.getters['inboxes/getInbox'](
this.$route.params.inbox_id
);
},
message() { message() {
if (!this.$route.params.website_token) { if (!this.$route.params.website_token) {
return this.$t('INBOX_MGMT.FINISH.MESSAGE'); return this.$t('INBOX_MGMT.FINISH.MESSAGE');
@ -40,7 +45,7 @@ export default {
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS'); return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
}, },
websiteScript() { 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"> <tr v-for="item in inboxesList" :key="item.id">
<td> <td>
<img <img
v-if="item.avatarUrl" v-if="item.avatar_url"
class="woot-thumbnail" class="woot-thumbnail"
:src="item.avatarUrl" :src="item.avatar_url"
alt="No Page Image" alt="No Page Image"
/> />
<img <img
@ -32,11 +32,11 @@
</td> </td>
<!-- Short Code --> <!-- Short Code -->
<td> <td>
<span class="agent-name">{{ item.label }}</span> <span class="agent-name">{{ item.name }}</span>
<span v-if="item.channelType === 'Channel::FacebookPage'"> <span v-if="item.channel_type === 'Channel::FacebookPage'">
Facebook Facebook
</span> </span>
<span v-if="item.channelType === 'Channel::WebWidget'"> <span v-if="item.channel_type === 'Channel::WebWidget'">
Website Website
</span> </span>
</td> </td>
@ -44,28 +44,23 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<td> <td>
<div class="button-wrapper"> <div class="button-wrapper">
<div v-if="isAdmin()" @click="openSettings(item)"> <router-link :to="`/app/settings/inboxes/${item.id}`">
<woot-submit-button <woot-submit-button
v-if="isAdmin()"
:button-text="$t('INBOX_MGMT.SETTINGS')" :button-text="$t('INBOX_MGMT.SETTINGS')"
icon-class="ion-gear-b" icon-class="ion-gear-b"
button-class="link hollow grey-btn" button-class="link hollow grey-btn"
/> />
</div> </router-link>
<!-- <div>
<woot-submit-button <woot-submit-button
:button-text="$t('INBOX_MGMT.REAUTH')" v-if="isAdmin()"
icon-class="ion-edit" :button-text="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[item.id]"
icon-class="ion-close-circled"
button-class="link hollow grey-btn" button-class="link hollow grey-btn"
/> @click="openDelete(item)"
</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>
</div> </div>
</td> </td>
</tr> </tr>
@ -120,22 +115,22 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
inboxesList: 'getInboxesList', inboxesList: 'inboxes/getInboxes',
}), }),
// Delete Modal // Delete Modal
deleteConfirmText() { deleteConfirmText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${ return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedInbox.label this.selectedInbox.name
}`; }`;
}, },
deleteRejectText() { deleteRejectText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${ return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedInbox.label this.selectedInbox.name
}`; }`;
}, },
deleteMessage() { deleteMessage() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${ return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedInbox.label this.selectedInbox.name
} ?`; } ?`;
}, },
}, },
@ -148,21 +143,19 @@ export default {
this.showSettings = false; this.showSettings = false;
this.selectedInbox = {}; this.selectedInbox = {};
}, },
deleteInbox({ channel_id }) { async deleteInbox({ id }) {
this.$store try {
.dispatch('deleteInbox', channel_id) await this.$store.dispatch('inboxes/delete', id);
.then(() => bus.$emit(
bus.$emit( 'newToastMessage',
'newToastMessage', this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
)
)
.catch(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
)
); );
} catch (error) {
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
);
}
}, },
confirmDeletion() { confirmDeletion() {

View file

@ -1,148 +1,170 @@
<template> <template>
<woot-modal class-name="settings-modal" :show.sync="show" :on-close="onClose"> <div class="settings columns container">
<div class="settings"> <woot-modal-header
<woot-modal-header :header-image="inbox.avatarUrl"
:header-image="inbox.avatarUrl" :header-title="inbox.name"
:header-title="inbox.label" />
/> <div
<div v-if="inbox.channel_type === 'Channel::FacebookPage'"
v-if="inbox.channelType === 'Channel::FacebookPage'" class="settings--content"
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 <settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')" :title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')" :sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
> >
</settings-form-header> </settings-form-header>
<woot-code :script="messengerScript"></woot-code> <woot-code :script="webWidgetScript"></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> -->
</div> </div>
<div class="settings--content"> <div class="settings--content">
<settings-form-header <settings-form-header
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')" :title="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')" :sub-title="
$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.PLACEHOLDER')
"
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')" :button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-updating="isUpdating" :is-updating="uiFlags.isUpdating"
@update="updateAgents" @update="updateWidgetColor"
> >
</settings-form-header> </settings-form-header>
<multiselect <Compact v-model="inbox.widget_color" class="widget-color--selector" />
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>
</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> </template>
<script> <script>
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* eslint-disable no-useless-escape */
/* global bus */ /* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { import {
createWebsiteWidgetScript, createWebsiteWidgetScript,
createMessengerScript, createMessengerScript,
} from 'dashboard/helper/scriptGenerator'; } from 'dashboard/helper/scriptGenerator';
import { Compact } from 'vue-color';
import SettingsFormHeader from '../../../../components/SettingsFormHeader.vue'; import SettingsFormHeader from '../../../../components/SettingsFormHeader.vue';
export default { export default {
components: { components: {
Compact,
SettingsFormHeader, SettingsFormHeader,
}, },
props: ['onClose', 'inbox', 'show'],
data() { data() {
return { return {
selectedAgents: [], selectedAgents: [],
isUpdating: false, isUpdating: false,
widgetColor: { hex: this.inbox.widgetColor }, isAgentListUpdating: false,
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
agentList: 'agents/getAgents', agentList: 'agents/getAgents',
uiFlags: 'inboxes/getUIFlags',
}), }),
currentInboxId() {
return this.$route.params.inboxId;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
},
webWidgetScript() { webWidgetScript() {
return createWebsiteWidgetScript(this.inbox.websiteToken); return createWebsiteWidgetScript(this.inbox.website_token);
}, },
messengerScript() { messengerScript() {
return createMessengerScript(this.inbox.pageId); return createMessengerScript(this.inbox.page_id);
}, },
}, },
mounted() { mounted() {
this.$store.dispatch('agents/get').then(() => { this.$store.dispatch('agents/get');
this.$store.dispatch('inboxes/get').then(() => {
this.fetchAttachedAgents(); this.fetchAttachedAgents();
}); });
}, },
methods: { methods: {
fetchAttachedAgents() { showAlert(message) {
this.$store bus.$emit('newToastMessage', message);
.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);
});
}, },
updateAgents() { async fetchAttachedAgents() {
const agentList = this.selectedAgents.map(el => el.id); try {
this.isUpdating = true; const response = await this.$store.dispatch('inboxMembers/get', {
this.$store inboxId: this.currentInboxId,
.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')
);
}); });
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: { validations: {

View file

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

View file

@ -55,8 +55,8 @@
</template> </template>
<script> <script>
/* global bus */
import { Compact } from 'vue-color'; import { Compact } from 'vue-color';
import { mapGetters } from 'vuex';
import router from '../../../../index'; import router from '../../../../index';
import PageHeader from '../../SettingsSubPageHeader'; import PageHeader from '../../SettingsSubPageHeader';
@ -73,26 +73,28 @@ export default {
isCreating: false, isCreating: false,
}; };
}, },
mounted() { computed: {
bus.$on('new_website_channel', ({ inboxId, websiteToken }) => { ...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({ router.replace({
name: 'settings_inboxes_add_agents', name: 'settings_inboxes_add_agents',
params: { params: {
page: 'new', page: 'new',
inbox_id: inboxId, inbox_id: website.id,
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,
}, },
}); });
}, },

View file

@ -1,5 +1,6 @@
/* eslint arrow-body-style: 0 */ /* eslint arrow-body-style: 0 */
import SettingsContent from '../Wrapper'; import SettingsContent from '../Wrapper';
import Settings from './Settings';
import InboxHome from './Index'; import InboxHome from './Index';
import InboxChannel from './InboxChannels'; import InboxChannel from './InboxChannels';
import ChannelList from './ChannelList'; 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 cannedResponse from './modules/cannedResponse';
import Channel from './modules/channels'; import Channel from './modules/channels';
import conversations from './modules/conversations'; import conversations from './modules/conversations';
import inboxes from './modules/inboxes';
import inboxMembers from './modules/inboxMembers';
import reports from './modules/reports'; import reports from './modules/reports';
import sideMenuItems from './modules/sidebar';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export default new Vuex.Store({
@ -19,7 +20,8 @@ export default new Vuex.Store({
cannedResponse, cannedResponse,
Channel, Channel,
conversations, conversations,
inboxes,
inboxMembers,
reports, 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_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX', SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
// labels // Inboxes
SET_LABELS: 'SET_LABELS', SET_INBOXES_UI_FLAG: 'SET_INBOXES_UI_FLAG',
// Set Inboxes
INBOXES_LOADING: 'INBOXES_LOADING',
SET_INBOXES: 'SET_INBOXES', SET_INBOXES: 'SET_INBOXES',
SET_INBOX_ITEM: 'SET_INBOX_ITEM', SET_INBOX_ITEM: 'SET_INBOX_ITEM',
DELETE_INBOX: 'DELETE_INBOX', ADD_INBOXES: 'ADD_INBOXES',
EDIT_INBOXES: 'EDIT_INBOXES',
DELETE_INBOXES: 'DELETE_INBOXES',
// Agent // Agent
SET_AGENT_FETCHING_STATUS: 'SET_AGENT_FETCHING_STATUS', SET_AGENT_FETCHING_STATUS: 'SET_AGENT_FETCHING_STATUS',

View file

@ -6,6 +6,15 @@ export const create = (state, data) => {
state.records.push(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) => { export const update = (state, data) => {
state.records.forEach((element, index) => { state.records.forEach((element, index) => {
if (element.id === data.id) { if (element.id === data.id) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,3 +3,4 @@ json.channel_id @inbox.channel_id
json.name @inbox.name json.name @inbox.name
json.channel_type @inbox.channel_type json.channel_type @inbox.channel_type
json.website_token @inbox.channel.try(:website_token) 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 namespace :widget do
resources :messages, only: [:index, :create] resources :messages, only: [:index, :create]
resources :inboxes, only: [:create] resources :inboxes, only: [:create, :update]
end end
namespace :actions do namespace :actions do
@ -92,7 +92,7 @@ Rails.application.routes.draw do
# Sidekiq Web UI # Sidekiq Web UI
require 'sidekiq/web' require 'sidekiq/web'
authenticate :user, lambda { |u| u.administrator? } do authenticate :user, ->(u) { u.administrator? } do
mount Sidekiq::Web => '/sidekiq' mount Sidekiq::Web => '/sidekiq'
end end

View file

@ -2,11 +2,13 @@ require 'rails_helper'
RSpec.describe '/api/v1/widget/inboxes', type: :request do RSpec.describe '/api/v1/widget/inboxes', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:admin) { create(:user, account: account, role: :administrator) } let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) } 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 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 context 'when unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
post '/api/v1/widget/inboxes', params: params 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 'when user is logged in' do
context 'with user as administrator' do context 'with user as administrator' do
it 'creates inbox and returns website_token' 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) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
@ -31,8 +33,43 @@ RSpec.describe '/api/v1/widget/inboxes', type: :request do
it 'returns unauthorized' do it 'returns unauthorized' do
post '/api/v1/widget/inboxes', post '/api/v1/widget/inboxes',
params: params, params: params,
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token
as: :json
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) expect(response).to have_http_status(:unauthorized)
end end

View file

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

View file

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