From d21c1c773b26eb87cdc938a3e8bcf596a778c3ab Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 14 Jun 2021 10:36:00 +0530 Subject: [PATCH] feat: Ability to add label for contact page (#2350) * feat: Ability to add label for contact page Co-authored-by: Muhsin Keloth Co-authored-by: Pranav Raj S Co-authored-by: Nithin David Thomas --- app/javascript/dashboard/api/contacts.js | 8 ++ .../dashboard/api/specs/contacts.spec.js | 19 +++ .../widgets/LabelSelector.stories.js | 67 ++++++++++ .../components/widgets/LabelSelector.vue | 116 ++++++++++++++++++ .../dashboard/i18n/locale/en/contact.json | 19 ++- .../contacts/components/ContactInfoPanel.vue | 11 +- .../contacts/components/ContactLabels.vue | 89 ++++++++++++++ .../conversation/labels/LabelBox.vue | 5 +- app/javascript/dashboard/store/index.js | 2 + .../dashboard/store/modules/contactLabels.js | 89 ++++++++++++++ .../specs/contactLabels/actions.spec.js | 73 +++++++++++ .../specs/contactLabels/getters.spec.js | 24 ++++ .../specs/contactLabels/mutations.spec.js | 29 +++++ .../dashboard/store/mutation-types.js | 4 + .../components/ui/dropdown/AddLabel.vue | 2 +- .../components/ui/label/LabelDropdown.vue | 4 - .../components/ui/label/LabelDropdownItem.vue | 33 +++-- 17 files changed, 562 insertions(+), 32 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/LabelSelector.stories.js create mode 100644 app/javascript/dashboard/components/widgets/LabelSelector.vue create mode 100644 app/javascript/dashboard/routes/dashboard/contacts/components/ContactLabels.vue create mode 100644 app/javascript/dashboard/store/modules/contactLabels.js create mode 100644 app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 1a66db0e1..cc8448873 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -18,6 +18,14 @@ class ContactAPI extends ApiClient { return axios.get(`${this.url}/${contactId}/contactable_inboxes`); } + getContactLabels(contactId) { + return axios.get(`${this.url}/${contactId}/labels`); + } + + updateContactLabels(contactId, labels) { + return axios.post(`${this.url}/${contactId}/labels`, { labels }); + } + search(search = '', page = 1, sortAttr = 'name') { return axios.get( `${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}` diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index ae1e1e0e1..a7080a634 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -34,6 +34,25 @@ describe('#ContactsAPI', () => { '/api/v1/contacts/1/contactable_inboxes' ); }); + + it('#getContactLabels', () => { + contactAPI.getContactLabels(1); + expect(context.axiosMock.get).toHaveBeenCalledWith( + '/api/v1/contacts/1/labels' + ); + }); + + it('#updateContactLabels', () => { + const labels = ['support-query']; + contactAPI.updateContactLabels(1, labels); + expect(context.axiosMock.post).toHaveBeenCalledWith( + '/api/v1/contacts/1/labels', + { + labels, + } + ); + }); + it('#search', () => { contactAPI.search('leads', 1, 'date'); expect(context.axiosMock.get).toHaveBeenCalledWith( diff --git a/app/javascript/dashboard/components/widgets/LabelSelector.stories.js b/app/javascript/dashboard/components/widgets/LabelSelector.stories.js new file mode 100644 index 000000000..77ca54b25 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/LabelSelector.stories.js @@ -0,0 +1,67 @@ +import { action } from '@storybook/addon-actions'; +import LabelSelector from './LabelSelector'; + +export default { + title: 'Components/Label/Contact Label', + component: LabelSelector, + argTypes: { + contactId: { + control: { + type: 'text ,number', + }, + }, + }, +}; + +const Template = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { LabelSelector }, + template: + '', +}); + +export const ContactLabel = Template.bind({}); +ContactLabel.args = { + onAdd: action('Added'), + onRemove: action('Removed'), + allLabels: [ + { + id: '1', + title: 'sales', + description: '', + color: '#0a5dd1', + }, + { + id: '2', + title: 'refund', + description: '', + color: '#8442f5', + }, + { + id: '3', + title: 'testing', + description: '', + color: '#f542f5', + }, + { + id: '4', + title: 'scheduled', + description: '', + color: '#42d1f5', + }, + ], + savedLabels: [ + { + id: '2', + title: 'refund', + description: '', + color: '#8442f5', + }, + { + id: '4', + title: 'scheduled', + description: '', + color: '#42d1f5', + }, + ], +}; diff --git a/app/javascript/dashboard/components/widgets/LabelSelector.vue b/app/javascript/dashboard/components/widgets/LabelSelector.vue new file mode 100644 index 000000000..8a6270bb8 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/LabelSelector.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 949637088..84e6303fc 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -18,19 +18,14 @@ "TITLE": "Previous Conversations" }, "LABELS": { - "TITLE": "Conversation Labels", - "MODAL": { - "TITLE": "Labels for", - "ACTIVE_LABELS": "Labels added to the conversation", - "INACTIVE_LABELS": "Labels available in the account", - "REMOVE": "Click on X icon to remove the label", - "ADD": "Click on + icon to add the label", - "ADD_BUTTON": "Add Labels", - "UPDATE_BUTTON": "Update labels", - "UPDATE_ERROR": "Couldn't update labels, try again." + "CONTACT": { + "TITLE": "Contact Labels", + "ERROR": "Couldn't update labels" + }, + "CONVERSATION": { + "TITLE": "Conversation Labels", + "ADD_BUTTON": "Add Labels" }, - "NO_LABELS_TO_ADD": "There are no more labels defined in the account.", - "NO_AVAILABLE_LABELS": "There are no labels added to this conversation.", "LABEL_SELECT": { "TITLE": "Add Labels", "PLACEHOLDER": "Search labels", diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue index 62970ec64..3ddfc0a59 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue @@ -8,6 +8,7 @@ v-if="hasContactAttributes" :custom-attributes="contact.custom_attributes" /> + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue b/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue index 56aef8547..162f853ed 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue @@ -5,7 +5,7 @@ class="contact-conversation--list" > @@ -30,7 +30,6 @@ v-if="showSearchDropdownLabel" :account-labels="accountLabels" :selected-labels="savedLabels" - :conversation-id="conversationId" @add="addItem" @remove="removeItem" /> @@ -61,7 +60,7 @@ export default { mixins: [clickaway], props: { conversationId: { - type: [String, Number], + type: Number, required: true, }, }, diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 867dcecf8..2dcf1f9e3 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -7,6 +7,7 @@ import auth from './modules/auth'; import cannedResponse from './modules/cannedResponse'; import contactConversations from './modules/contactConversations'; import contacts from './modules/contacts'; +import contactLabels from './modules/contactLabels'; import notifications from './modules/notifications'; import conversationLabels from './modules/conversationLabels'; import conversationMetadata from './modules/conversationMetadata'; @@ -38,6 +39,7 @@ export default new Vuex.Store({ cannedResponse, contactConversations, contacts, + contactLabels, notifications, conversationLabels, conversationMetadata, diff --git a/app/javascript/dashboard/store/modules/contactLabels.js b/app/javascript/dashboard/store/modules/contactLabels.js new file mode 100644 index 000000000..8ee6c09a6 --- /dev/null +++ b/app/javascript/dashboard/store/modules/contactLabels.js @@ -0,0 +1,89 @@ +import Vue from 'vue'; +import types from '../mutation-types'; +import ContactAPI from '../../api/contacts'; + +const state = { + records: {}, + uiFlags: { + isFetching: false, + isUpdating: false, + isError: false, + }, +}; + +export const getters = { + getUIFlags($state) { + return $state.uiFlags; + }, + getContactLabels: $state => id => { + return $state.records[Number(id)] || []; + }, +}; + +export const actions = { + get: async ({ commit }, contactId) => { + commit(types.SET_CONTACT_LABELS_UI_FLAG, { + isFetching: true, + }); + try { + const response = await ContactAPI.getContactLabels(contactId); + commit(types.SET_CONTACT_LABELS, { + id: contactId, + data: response.data.payload, + }); + commit(types.SET_CONTACT_LABELS_UI_FLAG, { + isFetching: false, + }); + } catch (error) { + commit(types.SET_CONTACT_LABELS_UI_FLAG, { + isFetching: false, + }); + } + }, + update: async ({ commit }, { contactId, labels }) => { + commit(types.SET_CONTACT_LABELS_UI_FLAG, { + isUpdating: true, + }); + try { + const response = await ContactAPI.updateContactLabels(contactId, labels); + commit(types.SET_CONTACT_LABELS, { + id: contactId, + data: response.data.payload, + }); + commit(types.SET_CONTACT_LABELS_UI_FLAG, { + isUpdating: false, + isError: false, + }); + } catch (error) { + commit(types.SET_CONTACT_LABELS_UI_FLAG, { + isUpdating: false, + isError: true, + }); + throw new Error(error); + } + }, + + setContactLabel({ commit }, { id, data }) { + commit(types.SET_CONTACT_LABELS, { id, data }); + }, +}; + +export const mutations = { + [types.SET_CONTACT_LABELS_UI_FLAG]($state, data) { + $state.uiFlags = { + ...$state.uiFlags, + ...data, + }; + }, + [types.SET_CONTACT_LABELS]: ($state, { id, data }) => { + Vue.set($state.records, id, data); + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js new file mode 100644 index 000000000..0f5222603 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js @@ -0,0 +1,73 @@ +import axios from 'axios'; +import { actions } from '../../contactLabels'; +import * as 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: { payload: ['customer-success', 'on-hold'] }, + }); + await actions.get({ commit }, 1); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CONTACT_LABELS_UI_FLAG, { isFetching: true }], + + [ + types.default.SET_CONTACT_LABELS, + { id: 1, data: ['customer-success', 'on-hold'] }, + ], + [types.default.SET_CONTACT_LABELS_UI_FLAG, { isFetching: false }], + ]); + }); + 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_CONTACT_LABELS_UI_FLAG, { isFetching: true }], + [types.default.SET_CONTACT_LABELS_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#update', () => { + it('updates correct actions if API is success', async () => { + axios.post.mockResolvedValue({ + data: { payload: { contactId: '1', labels: ['on-hold'] } }, + }); + await actions.update({ commit }, { contactId: '1', labels: ['on-hold'] }); + + expect(commit.mock.calls).toEqual([ + [types.default.SET_CONTACT_LABELS_UI_FLAG, { isUpdating: true }], + [ + types.default.SET_CONTACT_LABELS, + { + id: '1', + data: { contactId: '1', labels: ['on-hold'] }, + }, + ], + [ + types.default.SET_CONTACT_LABELS_UI_FLAG, + { isUpdating: false, isError: false }, + ], + ]); + }); + + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.update({ commit }, { contactId: '1', labels: ['on-hold'] }) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CONTACT_LABELS_UI_FLAG, { isUpdating: true }], + [ + types.default.SET_CONTACT_LABELS_UI_FLAG, + { isUpdating: false, isError: true }, + ], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js b/app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js new file mode 100644 index 000000000..9acb0decf --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js @@ -0,0 +1,24 @@ +import { getters } from '../../contactLabels'; + +describe('#getters', () => { + it('getContactLabels', () => { + const state = { + records: { 1: ['customer-success', 'on-hold'] }, + }; + expect(getters.getContactLabels(state)(1)).toEqual([ + 'customer-success', + 'on-hold', + ]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js new file mode 100644 index 000000000..099281363 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js @@ -0,0 +1,29 @@ +import * as types from '../../../mutation-types'; +import { mutations } from '../../contactLabels'; + +describe('#mutations', () => { + describe('#SET_CONTACT_LABELS_UI_FLAG', () => { + it('set ui flags', () => { + const state = { uiFlags: { isFetching: true } }; + mutations[types.default.SET_CONTACT_LABELS_UI_FLAG](state, { + isFetching: false, + }); + expect(state.uiFlags).toEqual({ + isFetching: false, + }); + }); + }); + + describe('#SET_CONTACT_LABELS', () => { + it('set contact labels', () => { + const state = { records: {} }; + mutations[types.default.SET_CONTACT_LABELS](state, { + id: 1, + data: ['customer-success', 'on-hold'], + }); + expect(state.records).toEqual({ + 1: ['customer-success', 'on-hold'], + }); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index f2d101302..7ccb6bccb 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -119,6 +119,10 @@ export default { SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION', + // Contact Label + SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG', + SET_CONTACT_LABELS: 'SET_CONTACT_LABELS', + // Conversation Label SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG', SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS', diff --git a/app/javascript/shared/components/ui/dropdown/AddLabel.vue b/app/javascript/shared/components/ui/dropdown/AddLabel.vue index 4254749c1..9b54450d9 100644 --- a/app/javascript/shared/components/ui/dropdown/AddLabel.vue +++ b/app/javascript/shared/components/ui/dropdown/AddLabel.vue @@ -2,7 +2,7 @@ diff --git a/app/javascript/shared/components/ui/label/LabelDropdown.vue b/app/javascript/shared/components/ui/label/LabelDropdown.vue index a6e2dd131..58bcda4c6 100644 --- a/app/javascript/shared/components/ui/label/LabelDropdown.vue +++ b/app/javascript/shared/components/ui/label/LabelDropdown.vue @@ -41,10 +41,6 @@ export default { }, props: { - conversationId: { - type: [String, Number], - required: true, - }, accountLabels: { type: Array, default: () => [], diff --git a/app/javascript/shared/components/ui/label/LabelDropdownItem.vue b/app/javascript/shared/components/ui/label/LabelDropdownItem.vue index e692bfd0b..68f2528e5 100644 --- a/app/javascript/shared/components/ui/label/LabelDropdownItem.vue +++ b/app/javascript/shared/components/ui/label/LabelDropdownItem.vue @@ -9,9 +9,11 @@ class="label-color--display" :style="{ backgroundColor: color }" /> - {{ title }} + {{ title }} + +
+
- @@ -47,9 +49,14 @@ export default { .item-wrap { display: flex; + ::v-deep .button__content { + width: 100%; + } + .button-wrap { display: flex; justify-content: space-between; + width: 100%; &.active { display: flex; @@ -59,14 +66,24 @@ export default { .name-label-wrap { display: flex; - } + min-width: 0; + width: 100%; - .label-color--display { - margin-right: var(--space-small); - } + .label-color--display { + margin-right: var(--space-small); + } - .icon { - font-size: var(--font-size-small); + .label-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.1; + padding-right: var(--space-small); + } + + .icon { + font-size: var(--font-size-small); + } } }