mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 02:32:29 +00:00 
			
		
		
		
	feat: Ability to add label for contact page (#2350)
* feat: Ability to add label for contact page Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Nithin David Thomas <webofnithin@gmail.com>
This commit is contained in:
		| @@ -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}` | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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: | ||||
|     '<label-selector v-bind="$props" @add="onAdd" @remove="onRemove"></label-selector>', | ||||
| }); | ||||
|  | ||||
| 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', | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
							
								
								
									
										116
									
								
								app/javascript/dashboard/components/widgets/LabelSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								app/javascript/dashboard/components/widgets/LabelSelector.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <h6 class="text-block-title"> | ||||
|       <i class="title-icon ion-pricetags" /> | ||||
|       {{ $t('CONTACT_PANEL.LABELS.CONTACT.TITLE') }} | ||||
|     </h6> | ||||
|     <div v-on-clickaway="closeDropdownLabel" class="label-wrap"> | ||||
|       <add-label @add="toggleLabels" /> | ||||
|       <woot-label | ||||
|         v-for="label in savedLabels" | ||||
|         :key="label.id" | ||||
|         :title="label.title" | ||||
|         :description="label.description" | ||||
|         :show-close="true" | ||||
|         :bg-color="label.color" | ||||
|         @click="removeItem" | ||||
|       /> | ||||
|       <div class="dropdown-wrap"> | ||||
|         <div | ||||
|           :class="{ 'dropdown-pane--open': showSearchDropdownLabel }" | ||||
|           class="dropdown-pane" | ||||
|         > | ||||
|           <label-dropdown | ||||
|             v-if="showSearchDropdownLabel" | ||||
|             :account-labels="allLabels" | ||||
|             :selected-labels="selectedLabels" | ||||
|             @add="addItem" | ||||
|             @remove="removeItem" | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import AddLabel from 'shared/components/ui/dropdown/AddLabel'; | ||||
| import LabelDropdown from 'shared/components/ui/label/LabelDropdown'; | ||||
| import { mixin as clickaway } from 'vue-clickaway'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     AddLabel, | ||||
|     LabelDropdown, | ||||
|   }, | ||||
|  | ||||
|   mixins: [clickaway], | ||||
|  | ||||
|   props: { | ||||
|     allLabels: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     savedLabels: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   data() { | ||||
|     return { | ||||
|       showSearchDropdownLabel: false, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     selectedLabels() { | ||||
|       return this.savedLabels.map(label => label.title); | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     addItem(label) { | ||||
|       this.$emit('add', label); | ||||
|     }, | ||||
|  | ||||
|     removeItem(label) { | ||||
|       this.$emit('remove', label); | ||||
|     }, | ||||
|  | ||||
|     toggleLabels() { | ||||
|       this.showSearchDropdownLabel = !this.showSearchDropdownLabel; | ||||
|     }, | ||||
|  | ||||
|     closeDropdownLabel() { | ||||
|       this.showSearchDropdownLabel = false; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .title-icon { | ||||
|   margin-right: var(--space-smaller); | ||||
| } | ||||
|  | ||||
| .label-wrap { | ||||
|   position: relative; | ||||
|   margin-left: var(--space-two); | ||||
|   line-height: var(--space-medium); | ||||
|  | ||||
|   .dropdown-wrap { | ||||
|     display: flex; | ||||
|     position: absolute; | ||||
|     margin-right: var(--space-medium); | ||||
|     top: var(--space-medium); | ||||
|     width: 100%; | ||||
|     left: -1px; | ||||
|  | ||||
|     .dropdown-pane { | ||||
|       width: 100%; | ||||
|       box-sizing: border-box; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -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", | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
|       v-if="hasContactAttributes" | ||||
|       :custom-attributes="contact.custom_attributes" | ||||
|     /> | ||||
|     <contact-label :contact-id="contact.id" class="contact-labels" /> | ||||
|     <contact-conversations | ||||
|       v-if="contact.id" | ||||
|       :contact-id="contact.id" | ||||
| @@ -20,12 +21,14 @@ | ||||
| import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations'; | ||||
| import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo'; | ||||
| import ContactCustomAttributes from 'dashboard/routes/dashboard/conversation/ContactCustomAttributes'; | ||||
| import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     ContactCustomAttributes, | ||||
|     ContactConversations, | ||||
|     ContactInfo, | ||||
|     ContactLabel, | ||||
|   }, | ||||
|   props: { | ||||
|     contact: { | ||||
| @@ -61,6 +64,10 @@ export default { | ||||
|   position: relative; | ||||
|   border-left: 1px solid var(--color-border); | ||||
|   padding: var(--space-medium) var(--space-two); | ||||
|    | ||||
|   .contact-labels { | ||||
|     padding-bottom: var(--space-normal); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .close-button { | ||||
| @@ -79,10 +86,6 @@ export default { | ||||
|   padding: 0 var(--space-normal); | ||||
| } | ||||
|  | ||||
| .contact-conversation--panel { | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| .contact--mute { | ||||
|   color: var(--r-400); | ||||
|   display: block; | ||||
|   | ||||
| @@ -0,0 +1,89 @@ | ||||
| <template> | ||||
|   <label-selector | ||||
|     :all-labels="allLabels" | ||||
|     :saved-labels="savedLabels" | ||||
|     @add="addItem" | ||||
|     @remove="removeItem" | ||||
|   /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
|  | ||||
| export default { | ||||
|   components: { LabelSelector }, | ||||
|   mixins: [alertMixin], | ||||
|   props: { | ||||
|     contactId: { | ||||
|       type: [String, Number], | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     savedLabels() { | ||||
|       const result = this.$store.getters['contactLabels/getContactLabels']( | ||||
|         this.contactId | ||||
|       ); | ||||
|       return result.map(value => { | ||||
|         return this.allLabels.find(label => label.title === value); | ||||
|       }); | ||||
|     }, | ||||
|  | ||||
|     ...mapGetters({ | ||||
|       labelUiFlags: 'contactLabels/getUIFlags', | ||||
|       allLabels: 'labels/getLabels', | ||||
|     }), | ||||
|   }, | ||||
|  | ||||
|   watch: { | ||||
|     contactId(newContactId, prevContactId) { | ||||
|       if (newContactId && newContactId !== prevContactId) { | ||||
|         this.fetchLabels(newContactId); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   mounted() { | ||||
|     const { contactId } = this; | ||||
|     this.fetchLabels(contactId); | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     async onUpdateLabels(selectedLabels) { | ||||
|       try { | ||||
|         await this.$store.dispatch('contactLabels/update', { | ||||
|           contactId: this.contactId, | ||||
|           labels: selectedLabels, | ||||
|         }); | ||||
|       } catch (error) { | ||||
|         this.showAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR')); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     addItem(value) { | ||||
|       const result = this.savedLabels.map(item => item.title); | ||||
|       result.push(value.title); | ||||
|       this.onUpdateLabels(result); | ||||
|     }, | ||||
|  | ||||
|     removeItem(value) { | ||||
|       const result = this.savedLabels | ||||
|         .map(label => label.title) | ||||
|         .filter(label => label !== value); | ||||
|       this.onUpdateLabels(result); | ||||
|     }, | ||||
|  | ||||
|     async fetchLabels(contactId) { | ||||
|       if (!contactId) { | ||||
|         return; | ||||
|       } | ||||
|       this.$store.dispatch('contactLabels/get', contactId); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style></style> | ||||
| @@ -5,7 +5,7 @@ | ||||
|       class="contact-conversation--list" | ||||
|     > | ||||
|       <contact-details-item | ||||
|         :title="$t('CONTACT_PANEL.LABELS.TITLE')" | ||||
|         :title="$t('CONTACT_PANEL.LABELS.CONVERSATION.TITLE')" | ||||
|         icon="ion-pricetags" | ||||
|         emoji="🏷️" | ||||
|       /> | ||||
| @@ -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, | ||||
|     }, | ||||
|   }, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										89
									
								
								app/javascript/dashboard/store/modules/contactLabels.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								app/javascript/dashboard/store/modules/contactLabels.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| }; | ||||
| @@ -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 }, | ||||
|         ], | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -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, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -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'], | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -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', | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <woot-button variant="link" class="label--add" @click="addLabel"> | ||||
|     <woot-label | ||||
|       color-scheme="secondary" | ||||
|       :title="$t('CONTACT_PANEL.LABELS.MODAL.ADD_BUTTON')" | ||||
|       :title="$t('CONTACT_PANEL.LABELS.CONVERSATION.ADD_BUTTON')" | ||||
|       icon="ion-plus-round" | ||||
|     /> | ||||
|   </woot-button> | ||||
|   | ||||
| @@ -41,10 +41,6 @@ export default { | ||||
|   }, | ||||
|  | ||||
|   props: { | ||||
|     conversationId: { | ||||
|       type: [String, Number], | ||||
|       required: true, | ||||
|     }, | ||||
|     accountLabels: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|   | ||||
| @@ -9,9 +9,11 @@ | ||||
|               class="label-color--display" | ||||
|               :style="{ backgroundColor: color }" | ||||
|             /> | ||||
|             <span>{{ title }}</span> | ||||
|             <span class="label-text" :title="title">{{ title }}</span> | ||||
|           </div> | ||||
|           <div> | ||||
|             <i v-if="selected" class="icon ion-checkmark-round" /> | ||||
|           </div> | ||||
|           <i v-if="selected" class="icon ion-checkmark-round" /> | ||||
|         </div> | ||||
|       </woot-button> | ||||
|     </div> | ||||
| @@ -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); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sivin Varghese
					Sivin Varghese