feat: Allow users to create dashboard apps to give agents more context (#4761)

This commit is contained in:
Pranav Raj S
2022-06-01 11:13:10 +05:30
committed by GitHub
parent 55f7be4ffc
commit b9aa4444b3
26 changed files with 585 additions and 21 deletions

View File

@@ -0,0 +1,9 @@
import ApiClient from './ApiClient';
class DashboardAppsAPI extends ApiClient {
constructor() {
super('dashboard_apps', { accountScoped: true });
}
}
export default new DashboardAppsAPI();

View File

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

View File

@@ -0,0 +1,64 @@
<template>
<div class="dashboard-app--container">
<div
v-for="(configItem, index) in config"
:key="index"
class="dashboard-app--list"
>
<iframe
v-if="configItem.type === 'frame' && configItem.url"
:id="`dashboard-app--frame-${index}`"
:src="configItem.url"
@load="() => onIframeLoad(index)"
/>
</div>
</div>
</template>
<script>
export default {
props: {
config: {
type: Array,
default: () => [],
},
currentChat: {
type: Object,
default: () => ({}),
},
},
computed: {
dashboardAppContext() {
return {
conversation: this.currentChat,
contact: this.$store.getters['contacts/getContact'](this.contactId),
};
},
contactId() {
return this.currentChat?.meta?.sender?.id;
},
},
methods: {
onIframeLoad(index) {
const frameElement = document.getElementById(
`dashboard-app--frame-${index}`
);
const eventData = { event: 'appContext', data: this.dashboardAppContext };
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
},
},
};
</script>
<style scoped>
.dashboard-app--container,
.dashboard-app--list,
.dashboard-app--list iframe {
height: 100%;
width: 100%;
}
.dashboard-app--list iframe {
border: 0;
}
</style>

View File

@@ -6,7 +6,20 @@
:is-contact-panel-open="isContactPanelOpen"
@contact-panel-toggle="onToggleContactPanel"
/>
<div class="messages-and-sidebar">
<woot-tabs
v-if="dashboardApps.length && currentChat.id"
:index="activeIndex"
class="dashboard-app--tabs"
@change="onDashboardAppTabChange"
>
<woot-tabs-item
v-for="tab in dashboardAppTabs"
:key="tab.key"
:name="tab.name"
:show-badge="false"
/>
</woot-tabs>
<div v-if="!activeIndex" class="messages-and-sidebar">
<messages-view
v-if="currentChat.id"
:inbox-id="inboxId"
@@ -14,7 +27,6 @@
@contact-panel-toggle="onToggleContactPanel"
/>
<empty-state v-else />
<div v-show="showContactPanel" class="conversation-sidebar-wrap">
<contact-panel
v-if="showContactPanel"
@@ -24,21 +36,29 @@
/>
</div>
</div>
<dashboard-app-frame
v-else
:key="currentChat.id"
:config="dashboardApps[activeIndex - 1].content"
:current-chat="currentChat"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel';
import ConversationHeader from './ConversationHeader';
import DashboardAppFrame from '../DashboardApp/Frame.vue';
import EmptyState from './EmptyState';
import MessagesView from './MessagesView';
export default {
components: {
EmptyState,
MessagesView,
ContactPanel,
ConversationHeader,
DashboardAppFrame,
EmptyState,
MessagesView,
},
props: {
@@ -52,8 +72,26 @@ export default {
default: true,
},
},
data() {
return { activeIndex: 0 };
},
computed: {
...mapGetters({ currentChat: 'getSelectedChat' }),
...mapGetters({
currentChat: 'getSelectedChat',
dashboardApps: 'dashboardApps/getRecords',
}),
dashboardAppTabs() {
return [
{
key: 'messages',
name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'),
},
...this.dashboardApps.map(dashboardApp => ({
key: `dashboard-${dashboardApp.id}`,
name: dashboardApp.title,
})),
];
},
showContactPanel() {
return this.isContactPanelOpen && this.currentChat.id;
},
@@ -70,6 +108,7 @@ export default {
},
mounted() {
this.fetchLabels();
this.$store.dispatch('dashboardApps/get');
},
methods: {
fetchLabels() {
@@ -81,6 +120,9 @@ export default {
onToggleContactPanel() {
this.$emit('contact-panel-toggle');
},
onDashboardAppTabChange(index) {
this.activeIndex = index;
},
},
};
</script>
@@ -96,6 +138,11 @@ export default {
background: var(--color-background-light);
}
.dashboard-app--tabs {
background: var(--white);
margin-top: -1px;
}
.messages-and-sidebar {
display: flex;
background: var(--color-background-light);

View File

@@ -1,6 +1,7 @@
{
"CONVERSATION": {
"404": "Please select a conversation from left pane",
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
"UNVERIFIED_SESSION": "The identity of this user is not verified",
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
"NO_MESSAGE_2": " to send a message to your page!",

View File

@@ -3,7 +3,9 @@ import Vuex from 'vuex';
import accounts from './modules/accounts';
import agents from './modules/agents';
import attributes from './modules/attributes';
import auth from './modules/auth';
import automations from './modules/automations';
import campaigns from './modules/campaigns';
import cannedResponse from './modules/cannedResponse';
import contactConversations from './modules/contactConversations';
@@ -18,6 +20,8 @@ import conversationSearch from './modules/conversationSearch';
import conversationStats from './modules/conversationStats';
import conversationTypingStatus from './modules/conversationTypingStatus';
import csat from './modules/csat';
import customViews from './modules/customViews';
import dashboardApps from './modules/dashboardApps';
import globalConfig from 'shared/store/globalConfig';
import inboxAssignableAgents from './modules/inboxAssignableAgents';
import inboxes from './modules/inboxes';
@@ -30,16 +34,15 @@ import teamMembers from './modules/teamMembers';
import teams from './modules/teams';
import userNotificationSettings from './modules/userNotificationSettings';
import webhooks from './modules/webhooks';
import attributes from './modules/attributes';
import automations from './modules/automations';
import customViews from './modules/customViews';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
accounts,
agents,
attributes,
auth,
automations,
campaigns,
cannedResponse,
contactConversations,
@@ -54,6 +57,8 @@ export default new Vuex.Store({
conversationStats,
conversationTypingStatus,
csat,
customViews,
dashboardApps,
globalConfig,
inboxAssignableAgents,
inboxes,
@@ -66,8 +71,5 @@ export default new Vuex.Store({
teams,
userNotificationSettings,
webhooks,
attributes,
automations,
customViews,
},
});

View File

@@ -0,0 +1,54 @@
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
import types from '../mutation-types';
import DashboardAppsAPI from '../../api/dashboardApps';
export const state = {
records: [],
uiFlags: {
isFetching: false,
isCreating: false,
isDeleting: false,
},
};
export const getters = {
getUIFlags(_state) {
return _state.uiFlags;
},
getRecords(_state) {
return _state.records;
},
};
export const actions = {
get: async function getDashboardApps({ commit }) {
commit(types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: true });
try {
const response = await DashboardAppsAPI.get();
commit(types.SET_DASHBOARD_APPS, response.data);
} catch (error) {
// Ignore error
} finally {
commit(types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: false });
}
},
};
export const mutations = {
[types.SET_DASHBOARD_APPS_UI_FLAG](_state, data) {
_state.uiFlags = {
..._state.uiFlags,
...data,
};
},
[types.SET_DASHBOARD_APPS]: MutationHelpers.set,
};
export default {
namespaced: true,
actions,
state,
getters,
mutations,
};

View File

@@ -0,0 +1,21 @@
import axios from 'axios';
import { actions } from '../../dashboardApps';
import types from '../../../mutation-types';
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: [{ title: 'Title 1' }] });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: true }],
[types.SET_DASHBOARD_APPS, [{ title: 'Title 1' }]],
[types.SET_DASHBOARD_APPS_UI_FLAG, { isFetching: false }],
]);
});
});
});

View File

@@ -0,0 +1,29 @@
import { getters } from '../../dashboardApps';
describe('#getters', () => {
it('getRecords', () => {
const state = {
records: [
{
title: '1',
content: [{ link: 'https://google.com', type: 'frame' }],
},
],
};
expect(getters.getRecords(state)).toEqual(state.records);
});
it('getUIFlags', () => {
const state = {
uiFlags: {
isFetching: true,
isCreating: false,
isDeleting: false,
},
};
expect(getters.getUIFlags(state)).toEqual({
isFetching: true,
isCreating: false,
isDeleting: false,
});
});
});

View File

@@ -0,0 +1,20 @@
import types from '../../../mutation-types';
import { mutations } from '../../dashboardApps';
describe('#mutations', () => {
describe('#SET_DASHBOARD_APPS_UI_FLAG', () => {
it('set dashboard app ui flags', () => {
const state = { uiFlags: { isCreating: false, isUpdating: false } };
mutations[types.SET_DASHBOARD_APPS_UI_FLAG](state, { isUpdating: true });
expect(state.uiFlags).toEqual({ isCreating: false, isUpdating: true });
});
});
describe('#SET_DASHBOARD_APPS', () => {
it('set dashboard records', () => {
const state = { records: [{ title: 'Title 0' }] };
mutations[types.SET_DASHBOARD_APPS](state, [{ title: 'Title 1' }]);
expect(state.records).toEqual([{ title: 'Title 1' }]);
});
});
});

View File

@@ -210,4 +210,8 @@ export default {
SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW',
ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
// Dashboard Apps
SET_DASHBOARD_APPS_UI_FLAG: 'SET_DASHBOARD_APPS_UI_FLAG',
SET_DASHBOARD_APPS: 'SET_DASHBOARD_APPS',
};

View File

@@ -61,7 +61,10 @@ export const actions = {
dispatch('conversationAttributes/getAttributes', {}, { root: true });
}
} catch (error) {
const data = error && error.response && error.response.data ? error.response.data : error
const data =
error && error.response && error.response.data
? error.response.data
: error;
IFrameHelper.sendMessage({
event: 'error',
errorType: SET_USER_ERROR,