diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 3f7b1a7a29b0d..dff9f0f77323c 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -4,13 +4,13 @@ v-if="!imgError && Boolean(src)" id="image" :src="src" - class="user-thumbnail" + :class="thumbnailClass" @error="onImgError()" /> { - if ( - typeof e.data !== 'string' || - e.data.indexOf('chatwoot-widget:') !== 0 - ) { - return; - } - const message = JSON.parse(e.data.replace('chatwoot-widget:', '')); + const wootPrefix = 'chatwoot-widget:'; + const isDataNotString = typeof e.data !== 'string'; + const isNotFromWoot = isDataNotString || e.data.indexOf(wootPrefix) !== 0; + + if (isNotFromWoot) return; + + const message = JSON.parse(e.data.replace(wootPrefix, '')); if (message.event === 'config-set') { this.fetchOldConversations(); + this.fetchAvailableAgents(websiteToken); } else if (message.event === 'widget-visible') { this.scrollConversationToBottom(); } else if (message.event === 'set-current-url') { @@ -44,6 +46,7 @@ export default { methods: { ...mapActions('appConfig', ['setWidgetColor']), ...mapActions('conversation', ['fetchOldConversations']), + ...mapActions('agent', ['fetchAvailableAgents']), scrollConversationToBottom() { const container = this.$el.querySelector('.conversation-wrap'); container.scrollTop = container.scrollHeight; diff --git a/app/javascript/widget/api/agent.js b/app/javascript/widget/api/agent.js new file mode 100644 index 0000000000000..0debeccafbed8 --- /dev/null +++ b/app/javascript/widget/api/agent.js @@ -0,0 +1,8 @@ +import endPoints from 'widget/api/endPoints'; +import { API } from 'widget/helpers/axios'; + +export const getAvailableAgents = async websiteToken => { + const urlData = endPoints.getAvailableAgents(websiteToken); + const result = await API.get(urlData.url, { params: urlData.params }); + return result; +}; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index efd231795ed75..2a36bc793e044 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -18,8 +18,16 @@ const updateContact = id => ({ url: `/api/v1/widget/messages/${id}${window.location.search}`, }); +const getAvailableAgents = token => ({ + url: '/api/v1/widget/inbox_members', + params: { + website_token: token, + }, +}); + export default { sendMessage, getConversation, updateContact, + getAvailableAgents, }; diff --git a/app/javascript/widget/assets/scss/_mixins.scss b/app/javascript/widget/assets/scss/_mixins.scss index e38b6488f9588..a338aef8c20d6 100755 --- a/app/javascript/widget/assets/scss/_mixins.scss +++ b/app/javascript/widget/assets/scss/_mixins.scss @@ -36,14 +36,14 @@ $color-shadow-outline: rgba(66, 153, 225, 0.5); } @mixin shadow { - box-shadow: 0 1px 10px -4 $color-shadow-medium, + box-shadow: 0 1px 10px 4px $color-shadow-medium, 0 1px 5px 2px $color-shadow-light; } @mixin shadow-medium { - box-shadow: 0 4px 6px -8px $color-shadow-medium, - 0 2px 4px -4px $color-shadow-light; + box-shadow: 0 4px 24px 8px $color-shadow-medium, + 0 2px 16px 4px $color-shadow-light; } diff --git a/app/javascript/widget/components/AvailableAgents.vue b/app/javascript/widget/components/AvailableAgents.vue new file mode 100644 index 0000000000000..dc48410e57605 --- /dev/null +++ b/app/javascript/widget/components/AvailableAgents.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue index 9522fa4bfe5df..148cc2b30b400 100644 --- a/app/javascript/widget/components/ChatHeader.vue +++ b/app/javascript/widget/components/ChatHeader.vue @@ -43,14 +43,10 @@ export default { .header-collapsed { display: flex; justify-content: space-between; - background: $color-white; padding: $space-two $space-medium; width: 100%; box-sizing: border-box; color: $color-white; - border-bottom-left-radius: $space-small; - border-bottom-right-radius: $space-small; - @include shadow-large; .title { font-size: $font-size-large; diff --git a/app/javascript/widget/components/ChatHeaderExpanded.vue b/app/javascript/widget/components/ChatHeaderExpanded.vue index d2f020ac61909..f7ff7034c7470 100755 --- a/app/javascript/widget/components/ChatHeaderExpanded.vue +++ b/app/javascript/widget/components/ChatHeaderExpanded.vue @@ -44,16 +44,9 @@ export default { @import '~widget/assets/scss/mixins.scss'; .header-expanded { - background: $color-white; padding: $space-larger $space-medium $space-large; width: 100%; box-sizing: border-box; - border-radius: $space-normal; - @include shadow-large; - - @media only screen and (min-device-width: 320px) and (max-device-width: 480px) { - border-radius: 0; - } .logo { width: 64px; @@ -71,7 +64,7 @@ export default { .body { color: $color-body; font-size: 1.8rem; - line-height: 1.6; + line-height: 1.5; } } diff --git a/app/javascript/widget/components/GroupedAvatars.vue b/app/javascript/widget/components/GroupedAvatars.vue new file mode 100644 index 0000000000000..709b9246f91f1 --- /dev/null +++ b/app/javascript/widget/components/GroupedAvatars.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/app/javascript/widget/helpers/specs/utils.spec.js b/app/javascript/widget/helpers/specs/utils.spec.js new file mode 100644 index 0000000000000..5c9312c333e4e --- /dev/null +++ b/app/javascript/widget/helpers/specs/utils.spec.js @@ -0,0 +1,26 @@ +import { getAvailableAgentsText } from '../utils'; + +describe('#getAvailableAgentsText', () => { + it('returns the correct text is there is only one online agent', () => { + expect(getAvailableAgentsText([{ name: 'Pranav' }])).toEqual( + 'Pranav is available' + ); + }); + + it('returns the correct text is there are two online agents', () => { + expect( + getAvailableAgentsText([{ name: 'Pranav' }, { name: 'Nithin' }]) + ).toEqual('Pranav and Nithin is available'); + }); + + it('returns the correct text is there are more than two online agents', () => { + expect( + getAvailableAgentsText([ + { name: 'Pranav' }, + { name: 'Nithin' }, + { name: 'Subin' }, + { name: 'Sojan' }, + ]) + ).toEqual('Pranav and 3 others are available'); + }); +}); diff --git a/app/javascript/widget/helpers/utils.js b/app/javascript/widget/helpers/utils.js index 42257bd4eb462..27dab35cbcf8c 100755 --- a/app/javascript/widget/helpers/utils.js +++ b/app/javascript/widget/helpers/utils.js @@ -18,3 +18,20 @@ export const IFrameHelper = { ); }, }; + +export const getAvailableAgentsText = (agents = []) => { + const count = agents.length; + if (count === 1) { + const [agent] = agents; + return `${agent.name} is available`; + } + + if (count === 2) { + const [first, second] = agents; + return `${first.name} and ${second.name} is available`; + } + + const [agent] = agents; + const rest = agents.length - 1; + return `${agent.name} and ${rest} others are available`; +}; diff --git a/app/javascript/widget/store/index.js b/app/javascript/widget/store/index.js index b63d254143c2c..d0a10b386a8b1 100755 --- a/app/javascript/widget/store/index.js +++ b/app/javascript/widget/store/index.js @@ -3,6 +3,7 @@ import Vuex from 'vuex'; import appConfig from 'widget/store/modules/appConfig'; import contact from 'widget/store/modules/contact'; import conversation from 'widget/store/modules/conversation'; +import agent from 'widget/store/modules/agent'; Vue.use(Vuex); @@ -11,5 +12,6 @@ export default new Vuex.Store({ appConfig, contact, conversation, + agent, }, }); diff --git a/app/javascript/widget/store/modules/agent.js b/app/javascript/widget/store/modules/agent.js new file mode 100644 index 0000000000000..2cdde0e0c2075 --- /dev/null +++ b/app/javascript/widget/store/modules/agent.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import { getAvailableAgents } from 'widget/api/agent'; + +const state = { + records: [], + uiFlags: { + isError: false, + hasFetched: false, + }, +}; + +export const getters = { + availableAgents: $state => + $state.records.filter(agent => agent.availability_status === 'online'), +}; + +export const actions = { + fetchAvailableAgents: async ({ commit }, websiteToken) => { + try { + const { data } = await getAvailableAgents(websiteToken); + const { payload = [] } = data; + commit('setAgents', payload); + commit('setError', false); + commit('setHasFetched', true); + } catch (error) { + commit('setError', true); + commit('setHasFetched', true); + } + }, +}; + +export const mutations = { + setAgents($state, data) { + Vue.set($state, 'records', data); + }, + setError($state, value) { + Vue.set($state.uiFlags, 'isError', value); + }, + setHasFetched($state, value) { + Vue.set($state.uiFlags, 'hasFetched', value); + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/widget/store/modules/specs/agent/actions.spec.js b/app/javascript/widget/store/modules/specs/agent/actions.spec.js new file mode 100644 index 0000000000000..5f1400dee319e --- /dev/null +++ b/app/javascript/widget/store/modules/specs/agent/actions.spec.js @@ -0,0 +1,28 @@ +import { API } from 'widget/helpers/axios'; +import { actions } from '../../agent'; +import { agents } from './data'; + +const commit = jest.fn(); +jest.mock('widget/helpers/axios'); + +describe('#actions', () => { + describe('#fetchAvailableAgents', () => { + it('sends correct actions if API is success', async () => { + API.get.mockResolvedValue({ data: { payload: agents } }); + await actions.fetchAvailableAgents({ commit }, 'Hi'); + expect(commit.mock.calls).toEqual([ + ['setAgents', agents], + ['setError', false], + ['setHasFetched', true], + ]); + }); + it('sends correct actions if API is error', async () => { + API.get.mockRejectedValue({ message: 'Authentication required' }); + await actions.fetchAvailableAgents({ commit }, 'Hi'); + expect(commit.mock.calls).toEqual([ + ['setError', true], + ['setHasFetched', true], + ]); + }); + }); +}); diff --git a/app/javascript/widget/store/modules/specs/agent/data.js b/app/javascript/widget/store/modules/specs/agent/data.js new file mode 100644 index 0000000000000..d66bc7dfa73c5 --- /dev/null +++ b/app/javascript/widget/store/modules/specs/agent/data.js @@ -0,0 +1,26 @@ +export const agents = [ + { + id: 1, + name: 'John', + avatar_url: '', + availability_status: 'online', + }, + { + id: 2, + name: 'Xavier', + avatar_url: '', + availability_status: 'offline', + }, + { + id: 3, + name: 'Pranav', + avatar_url: '', + availability_status: 'online', + }, + { + id: 4, + name: 'Nithin', + avatar_url: '', + availability_status: 'online', + }, +]; diff --git a/app/javascript/widget/store/modules/specs/agent/getters.spec.js b/app/javascript/widget/store/modules/specs/agent/getters.spec.js new file mode 100644 index 0000000000000..f70e3092d47f7 --- /dev/null +++ b/app/javascript/widget/store/modules/specs/agent/getters.spec.js @@ -0,0 +1,30 @@ +import { getters } from '../../agent'; +import { agents } from './data'; + +describe('#getters', () => { + it('availableAgents', () => { + const state = { + records: agents, + }; + expect(getters.availableAgents(state)).toEqual([ + { + id: 1, + name: 'John', + avatar_url: '', + availability_status: 'online', + }, + { + id: 3, + name: 'Pranav', + avatar_url: '', + availability_status: 'online', + }, + { + id: 4, + name: 'Nithin', + avatar_url: '', + availability_status: 'online', + }, + ]); + }); +}); diff --git a/app/javascript/widget/store/modules/specs/agent/mutations.spec.js b/app/javascript/widget/store/modules/specs/agent/mutations.spec.js new file mode 100644 index 0000000000000..77bf5ac1820f4 --- /dev/null +++ b/app/javascript/widget/store/modules/specs/agent/mutations.spec.js @@ -0,0 +1,28 @@ +import { mutations } from '../../agent'; +import agents from './data'; + +describe('#mutations', () => { + describe('#setAgents', () => { + it('set agent records', () => { + const state = { records: [] }; + mutations.setAgents(state, agents); + expect(state.records).toEqual(agents); + }); + }); + + describe('#setError', () => { + it('set error flag', () => { + const state = { records: [], uiFlags: {} }; + mutations.setError(state, true); + expect(state.uiFlags.isError).toEqual(true); + }); + }); + + describe('#setError', () => { + it('set fetched flag', () => { + const state = { records: [], uiFlags: {} }; + mutations.setHasFetched(state, true); + expect(state.uiFlags.hasFetched).toEqual(true); + }); + }); +}); diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue index 8d5d64742312a..36b2c6646384a 100755 --- a/app/javascript/widget/views/Home.vue +++ b/app/javascript/widget/views/Home.vue @@ -4,6 +4,7 @@ +