mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +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`); |     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') { |   search(search = '', page = 1, sortAttr = 'name') { | ||||||
|     return axios.get( |     return axios.get( | ||||||
|       `${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}` |       `${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}` | ||||||
|   | |||||||
| @@ -34,6 +34,25 @@ describe('#ContactsAPI', () => { | |||||||
|         '/api/v1/contacts/1/contactable_inboxes' |         '/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', () => { |     it('#search', () => { | ||||||
|       contactAPI.search('leads', 1, 'date'); |       contactAPI.search('leads', 1, 'date'); | ||||||
|       expect(context.axiosMock.get).toHaveBeenCalledWith( |       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" |       "TITLE": "Previous Conversations" | ||||||
|     }, |     }, | ||||||
|     "LABELS": { |     "LABELS": { | ||||||
|       "TITLE": "Conversation Labels", |       "CONTACT": { | ||||||
|       "MODAL": { |         "TITLE": "Contact Labels", | ||||||
|         "TITLE": "Labels for", |         "ERROR": "Couldn't update labels" | ||||||
|         "ACTIVE_LABELS": "Labels added to the conversation", |       }, | ||||||
|         "INACTIVE_LABELS": "Labels available in the account", |       "CONVERSATION": { | ||||||
|         "REMOVE": "Click on X icon to remove the label", |         "TITLE": "Conversation Labels", | ||||||
|         "ADD": "Click on + icon to add the label", |         "ADD_BUTTON": "Add Labels" | ||||||
|         "ADD_BUTTON": "Add Labels", |  | ||||||
|         "UPDATE_BUTTON": "Update labels", |  | ||||||
|         "UPDATE_ERROR": "Couldn't update labels, try again." |  | ||||||
|       }, |       }, | ||||||
|       "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": { |       "LABEL_SELECT": { | ||||||
|         "TITLE": "Add Labels", |         "TITLE": "Add Labels", | ||||||
|         "PLACEHOLDER": "Search labels", |         "PLACEHOLDER": "Search labels", | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|       v-if="hasContactAttributes" |       v-if="hasContactAttributes" | ||||||
|       :custom-attributes="contact.custom_attributes" |       :custom-attributes="contact.custom_attributes" | ||||||
|     /> |     /> | ||||||
|  |     <contact-label :contact-id="contact.id" class="contact-labels" /> | ||||||
|     <contact-conversations |     <contact-conversations | ||||||
|       v-if="contact.id" |       v-if="contact.id" | ||||||
|       :contact-id="contact.id" |       :contact-id="contact.id" | ||||||
| @@ -20,12 +21,14 @@ | |||||||
| import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations'; | import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations'; | ||||||
| import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo'; | import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo'; | ||||||
| import ContactCustomAttributes from 'dashboard/routes/dashboard/conversation/ContactCustomAttributes'; | import ContactCustomAttributes from 'dashboard/routes/dashboard/conversation/ContactCustomAttributes'; | ||||||
|  | import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     ContactCustomAttributes, |     ContactCustomAttributes, | ||||||
|     ContactConversations, |     ContactConversations, | ||||||
|     ContactInfo, |     ContactInfo, | ||||||
|  |     ContactLabel, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     contact: { |     contact: { | ||||||
| @@ -61,6 +64,10 @@ export default { | |||||||
|   position: relative; |   position: relative; | ||||||
|   border-left: 1px solid var(--color-border); |   border-left: 1px solid var(--color-border); | ||||||
|   padding: var(--space-medium) var(--space-two); |   padding: var(--space-medium) var(--space-two); | ||||||
|  |    | ||||||
|  |   .contact-labels { | ||||||
|  |     padding-bottom: var(--space-normal); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .close-button { | .close-button { | ||||||
| @@ -79,10 +86,6 @@ export default { | |||||||
|   padding: 0 var(--space-normal); |   padding: 0 var(--space-normal); | ||||||
| } | } | ||||||
|  |  | ||||||
| .contact-conversation--panel { |  | ||||||
|   height: 100%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .contact--mute { | .contact--mute { | ||||||
|   color: var(--r-400); |   color: var(--r-400); | ||||||
|   display: block; |   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" |       class="contact-conversation--list" | ||||||
|     > |     > | ||||||
|       <contact-details-item |       <contact-details-item | ||||||
|         :title="$t('CONTACT_PANEL.LABELS.TITLE')" |         :title="$t('CONTACT_PANEL.LABELS.CONVERSATION.TITLE')" | ||||||
|         icon="ion-pricetags" |         icon="ion-pricetags" | ||||||
|         emoji="🏷️" |         emoji="🏷️" | ||||||
|       /> |       /> | ||||||
| @@ -30,7 +30,6 @@ | |||||||
|               v-if="showSearchDropdownLabel" |               v-if="showSearchDropdownLabel" | ||||||
|               :account-labels="accountLabels" |               :account-labels="accountLabels" | ||||||
|               :selected-labels="savedLabels" |               :selected-labels="savedLabels" | ||||||
|               :conversation-id="conversationId" |  | ||||||
|               @add="addItem" |               @add="addItem" | ||||||
|               @remove="removeItem" |               @remove="removeItem" | ||||||
|             /> |             /> | ||||||
| @@ -61,7 +60,7 @@ export default { | |||||||
|   mixins: [clickaway], |   mixins: [clickaway], | ||||||
|   props: { |   props: { | ||||||
|     conversationId: { |     conversationId: { | ||||||
|       type: [String, Number], |       type: Number, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import auth from './modules/auth'; | |||||||
| import cannedResponse from './modules/cannedResponse'; | import cannedResponse from './modules/cannedResponse'; | ||||||
| import contactConversations from './modules/contactConversations'; | import contactConversations from './modules/contactConversations'; | ||||||
| import contacts from './modules/contacts'; | import contacts from './modules/contacts'; | ||||||
|  | import contactLabels from './modules/contactLabels'; | ||||||
| import notifications from './modules/notifications'; | import notifications from './modules/notifications'; | ||||||
| import conversationLabels from './modules/conversationLabels'; | import conversationLabels from './modules/conversationLabels'; | ||||||
| import conversationMetadata from './modules/conversationMetadata'; | import conversationMetadata from './modules/conversationMetadata'; | ||||||
| @@ -38,6 +39,7 @@ export default new Vuex.Store({ | |||||||
|     cannedResponse, |     cannedResponse, | ||||||
|     contactConversations, |     contactConversations, | ||||||
|     contacts, |     contacts, | ||||||
|  |     contactLabels, | ||||||
|     notifications, |     notifications, | ||||||
|     conversationLabels, |     conversationLabels, | ||||||
|     conversationMetadata, |     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', |   SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', | ||||||
|   ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION', |   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 |   // Conversation Label | ||||||
|   SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG', |   SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG', | ||||||
|   SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS', |   SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS', | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|   <woot-button variant="link" class="label--add" @click="addLabel"> |   <woot-button variant="link" class="label--add" @click="addLabel"> | ||||||
|     <woot-label |     <woot-label | ||||||
|       color-scheme="secondary" |       color-scheme="secondary" | ||||||
|       :title="$t('CONTACT_PANEL.LABELS.MODAL.ADD_BUTTON')" |       :title="$t('CONTACT_PANEL.LABELS.CONVERSATION.ADD_BUTTON')" | ||||||
|       icon="ion-plus-round" |       icon="ion-plus-round" | ||||||
|     /> |     /> | ||||||
|   </woot-button> |   </woot-button> | ||||||
|   | |||||||
| @@ -41,10 +41,6 @@ export default { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   props: { |   props: { | ||||||
|     conversationId: { |  | ||||||
|       type: [String, Number], |  | ||||||
|       required: true, |  | ||||||
|     }, |  | ||||||
|     accountLabels: { |     accountLabels: { | ||||||
|       type: Array, |       type: Array, | ||||||
|       default: () => [], |       default: () => [], | ||||||
|   | |||||||
| @@ -9,10 +9,12 @@ | |||||||
|               class="label-color--display" |               class="label-color--display" | ||||||
|               :style="{ backgroundColor: color }" |               :style="{ backgroundColor: color }" | ||||||
|             /> |             /> | ||||||
|             <span>{{ title }}</span> |             <span class="label-text" :title="title">{{ title }}</span> | ||||||
|           </div> |           </div> | ||||||
|  |           <div> | ||||||
|             <i v-if="selected" class="icon ion-checkmark-round" /> |             <i v-if="selected" class="icon ion-checkmark-round" /> | ||||||
|           </div> |           </div> | ||||||
|  |         </div> | ||||||
|       </woot-button> |       </woot-button> | ||||||
|     </div> |     </div> | ||||||
|   </woot-dropdown-item> |   </woot-dropdown-item> | ||||||
| @@ -47,9 +49,14 @@ export default { | |||||||
| .item-wrap { | .item-wrap { | ||||||
|   display: flex; |   display: flex; | ||||||
|  |  | ||||||
|  |   ::v-deep .button__content { | ||||||
|  |     width: 100%; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .button-wrap { |   .button-wrap { | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: space-between; |     justify-content: space-between; | ||||||
|  |     width: 100%; | ||||||
|  |  | ||||||
|     &.active { |     &.active { | ||||||
|       display: flex; |       display: flex; | ||||||
| @@ -59,16 +66,26 @@ export default { | |||||||
|  |  | ||||||
|     .name-label-wrap { |     .name-label-wrap { | ||||||
|       display: flex; |       display: flex; | ||||||
|     } |       min-width: 0; | ||||||
|  |       width: 100%; | ||||||
|  |  | ||||||
|       .label-color--display { |       .label-color--display { | ||||||
|         margin-right: var(--space-small); |         margin-right: var(--space-small); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       .label-text { | ||||||
|  |         overflow: hidden; | ||||||
|  |         text-overflow: ellipsis; | ||||||
|  |         white-space: nowrap; | ||||||
|  |         line-height: 1.1; | ||||||
|  |         padding-right: var(--space-small); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       .icon { |       .icon { | ||||||
|         font-size: var(--font-size-small); |         font-size: var(--font-size-small); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .label-color--display { |   .label-color--display { | ||||||
|     border-radius: var(--border-radius-normal); |     border-radius: var(--border-radius-normal); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sivin Varghese
					Sivin Varghese