mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	Feature: View conversation labels on sidebar (#436)
This commit is contained in:
		
							
								
								
									
										18
									
								
								app/javascript/dashboard/api/conversations.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/javascript/dashboard/api/conversations.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| /* global axios */ | ||||
| import ApiClient from './ApiClient'; | ||||
|  | ||||
| class ConversationApi extends ApiClient { | ||||
|   constructor() { | ||||
|     super('conversations'); | ||||
|   } | ||||
|  | ||||
|   getLabels(conversationID) { | ||||
|     return axios.get(`${this.url}/${conversationID}/labels`); | ||||
|   } | ||||
|  | ||||
|   createLabels(conversationID) { | ||||
|     return axios.get(`${this.url}/${conversationID}/labels`); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default new ConversationApi(); | ||||
							
								
								
									
										15
									
								
								app/javascript/dashboard/api/specs/conversations.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/javascript/dashboard/api/specs/conversations.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import conversations from '../conversations'; | ||||
| import ApiClient from '../ApiClient'; | ||||
|  | ||||
| describe('#ConversationApi', () => { | ||||
|   it('creates correct instance', () => { | ||||
|     expect(conversations).toBeInstanceOf(ApiClient); | ||||
|     expect(conversations).toHaveProperty('get'); | ||||
|     expect(conversations).toHaveProperty('show'); | ||||
|     expect(conversations).toHaveProperty('create'); | ||||
|     expect(conversations).toHaveProperty('update'); | ||||
|     expect(conversations).toHaveProperty('delete'); | ||||
|     expect(conversations).toHaveProperty('getLabels'); | ||||
|     expect(conversations).toHaveProperty('createLabels'); | ||||
|   }); | ||||
| }); | ||||
| @@ -7,6 +7,10 @@ | ||||
|     "CONVERSATIONS": { | ||||
|       "NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.", | ||||
|       "TITLE": "Previous Conversations" | ||||
|     }, | ||||
|     "LABELS": { | ||||
|       "NO_RECORDS_FOUND": "There are no labels associated to this conversation.", | ||||
|       "TITLE": "Labels" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -59,18 +59,22 @@ | ||||
|       :contact-id="contact.id" | ||||
|       :conversation-id="conversationId" | ||||
|     /> | ||||
|  | ||||
|     <conversation-labels :conversation-id="conversationId" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; | ||||
| import ContactDetailsItem from './ContactDetailsItem.vue'; | ||||
| import ContactConversations from './ContactConversations.vue'; | ||||
| import ContactDetailsItem from './ContactDetailsItem.vue'; | ||||
| import ConversationLabels from './ConversationLabels.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     ContactConversations, | ||||
|     ContactDetailsItem, | ||||
|     ConversationLabels, | ||||
|     Thumbnail, | ||||
|   }, | ||||
|   props: { | ||||
|   | ||||
| @@ -0,0 +1,83 @@ | ||||
| <template> | ||||
|   <div class="contact-conversation--panel"> | ||||
|     <contact-details-item | ||||
|       icon="ion-pricetags" | ||||
|       :title="$t('CONTACT_PANEL.LABELS.TITLE')" | ||||
|     /> | ||||
|     <div v-if="!uiFlags.isFetching"> | ||||
|       <i v-if="!labels.length"> | ||||
|         {{ $t('CONTACT_PANEL.LABELS.NO_RECORDS_FOUND') }} | ||||
|       </i> | ||||
|       <div v-else class="contact-conversation--list"> | ||||
|         <span | ||||
|           v-for="label in labels" | ||||
|           :key="label" | ||||
|           class="conversation--label label primary" | ||||
|         > | ||||
|           {{ label }} | ||||
|         </span> | ||||
|       </div> | ||||
|     </div> | ||||
|     <spinner v-else></spinner> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import Spinner from 'shared/components/Spinner.vue'; | ||||
| import ContactDetailsItem from './ContactDetailsItem.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     ContactDetailsItem, | ||||
|     Spinner, | ||||
|   }, | ||||
|   props: { | ||||
|     conversationId: { | ||||
|       type: [String, Number], | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     labels() { | ||||
|       return this.$store.getters['conversationLabels/getConversationLabels']( | ||||
|         this.conversationId | ||||
|       ); | ||||
|     }, | ||||
|     ...mapGetters({ | ||||
|       uiFlags: 'contactConversations/getUIFlags', | ||||
|     }), | ||||
|   }, | ||||
|   watch: { | ||||
|     conversationId(newConversationId, prevConversationId) { | ||||
|       if (newConversationId && newConversationId !== prevConversationId) { | ||||
|         this.$store.dispatch('conversationLabels/get', newConversationId); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$store.dispatch('conversationLabels/get', this.conversationId); | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import '~dashboard/assets/scss/variables'; | ||||
| @import '~dashboard/assets/scss/mixins'; | ||||
|  | ||||
| .contact-conversation--panel { | ||||
|   @include border-normal-top; | ||||
|   padding: $space-medium; | ||||
| } | ||||
|  | ||||
| .contact-conversation--list { | ||||
|   margin-top: -$space-normal; | ||||
| } | ||||
|  | ||||
| .conversation--label { | ||||
|   color: $color-white; | ||||
|   margin-right: $space-small; | ||||
|   font-size: $font-size-small; | ||||
|   padding: $space-smaller; | ||||
| } | ||||
| </style> | ||||
| @@ -9,6 +9,7 @@ import Channel from './modules/channels'; | ||||
| import contacts from './modules/contacts'; | ||||
| import contactConversations from './modules/contactConversations'; | ||||
| import conversationMetadata from './modules/conversationMetadata'; | ||||
| import conversationLabels from './modules/conversationLabels'; | ||||
| import conversations from './modules/conversations'; | ||||
| import inboxes from './modules/inboxes'; | ||||
| import inboxMembers from './modules/inboxMembers'; | ||||
| @@ -24,6 +25,7 @@ export default new Vuex.Store({ | ||||
|     Channel, | ||||
|     contacts, | ||||
|     contactConversations, | ||||
|     conversationLabels, | ||||
|     conversationMetadata, | ||||
|     conversations, | ||||
|     inboxes, | ||||
|   | ||||
							
								
								
									
										61
									
								
								app/javascript/dashboard/store/modules/conversationLabels.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								app/javascript/dashboard/store/modules/conversationLabels.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| import Vue from 'vue'; | ||||
| import * as types from '../mutation-types'; | ||||
| import ConversationAPI from '../../api/conversations'; | ||||
|  | ||||
| const state = { | ||||
|   records: {}, | ||||
|   uiFlags: { | ||||
|     isFetching: false, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const getters = { | ||||
|   getUIFlags($state) { | ||||
|     return $state.uiFlags; | ||||
|   }, | ||||
|   getConversationLabels: $state => id => { | ||||
|     return $state.records[Number(id)] || []; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const actions = { | ||||
|   get: async ({ commit }, conversationId) => { | ||||
|     commit(types.default.SET_CONVERSATION_LABELS_UI_FLAG, { | ||||
|       isFetching: true, | ||||
|     }); | ||||
|     try { | ||||
|       const response = await ConversationAPI.getLabels(conversationId); | ||||
|       commit(types.default.SET_CONVERSATION_LABELS, { | ||||
|         id: conversationId, | ||||
|         data: response.data.payload, | ||||
|       }); | ||||
|       commit(types.default.SET_CONVERSATION_LABELS_UI_FLAG, { | ||||
|         isFetching: false, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       commit(types.default.SET_CONVERSATION_LABELS_UI_FLAG, { | ||||
|         isFetching: false, | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const mutations = { | ||||
|   [types.default.SET_CONVERSATION_LABELS_UI_FLAG]($state, data) { | ||||
|     $state.uiFlags = { | ||||
|       ...$state.uiFlags, | ||||
|       ...data, | ||||
|     }; | ||||
|   }, | ||||
|   [types.default.SET_CONVERSATION_LABELS]: ($state, { id, data }) => { | ||||
|     Vue.set($state.records, id, data); | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   namespaced: true, | ||||
|   state, | ||||
|   getters, | ||||
|   actions, | ||||
|   mutations, | ||||
| }; | ||||
| @@ -0,0 +1,35 @@ | ||||
| import axios from 'axios'; | ||||
| import { actions } from '../../conversationLabels'; | ||||
| 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_CONVERSATION_LABELS_UI_FLAG, { isFetching: true }], | ||||
|  | ||||
|         [ | ||||
|           types.default.SET_CONVERSATION_LABELS, | ||||
|           { id: 1, data: ['customer-success', 'on-hold'] }, | ||||
|         ], | ||||
|         [types.default.SET_CONVERSATION_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_CONVERSATION_LABELS_UI_FLAG, { isFetching: true }], | ||||
|         [types.default.SET_CONVERSATION_LABELS_UI_FLAG, { isFetching: false }], | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,24 @@ | ||||
| import { getters } from '../../conversationLabels'; | ||||
|  | ||||
| describe('#getters', () => { | ||||
|   it('getConversationLabels', () => { | ||||
|     const state = { | ||||
|       records: { 1: ['customer-success', 'on-hold'] }, | ||||
|     }; | ||||
|     expect(getters.getConversationLabels(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 '../../conversationLabels'; | ||||
|  | ||||
| describe('#mutations', () => { | ||||
|   describe('#SET_CONVERSATION_LABELS_UI_FLAG', () => { | ||||
|     it('set ui flags', () => { | ||||
|       const state = { uiFlags: { isFetching: true } }; | ||||
|       mutations[types.default.SET_CONVERSATION_LABELS_UI_FLAG](state, { | ||||
|         isFetching: false, | ||||
|       }); | ||||
|       expect(state.uiFlags).toEqual({ | ||||
|         isFetching: false, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('#SET_CONVERSATION_LABELS', () => { | ||||
|     it('set contact conversation records', () => { | ||||
|       const state = { records: {} }; | ||||
|       mutations[types.default.SET_CONVERSATION_LABELS](state, { | ||||
|         id: 1, | ||||
|         data: ['customer-success', 'on-hold'], | ||||
|       }); | ||||
|       expect(state.records).toEqual({ | ||||
|         1: ['customer-success', 'on-hold'], | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -66,6 +66,10 @@ export default { | ||||
|   SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG', | ||||
|   SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', | ||||
|  | ||||
|   // Conversation Label | ||||
|   SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG', | ||||
|   SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS', | ||||
|  | ||||
|   // Reports | ||||
|   SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS', | ||||
|   SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY', | ||||
|   | ||||
| @@ -1,8 +1 @@ | ||||
| json.data do | ||||
|   json.meta do | ||||
|   end | ||||
|  | ||||
|   json.payload do | ||||
|     json.labels @labels | ||||
|   end | ||||
| end | ||||
| json.payload @labels | ||||
|   | ||||
| @@ -1,8 +1 @@ | ||||
| json.data do | ||||
|   json.meta do | ||||
|   end | ||||
|  | ||||
|   json.payload do | ||||
|     json.labels @labels | ||||
|   end | ||||
| end | ||||
| json.payload @labels | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S