mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	Feat: Show notes panel on crm page (#2320)
* Feat: Show notes panel on CRM page Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
		 Nithin David Thomas
					Nithin David Thomas
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							75d05e55ae
						
					
				
				
					commit
					fe2af370e0
				
			
							
								
								
									
										9
									
								
								app/javascript/dashboard/api/contactNotes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/javascript/dashboard/api/contactNotes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import ApiClient from './ApiClient'; | ||||
|  | ||||
| class ContactNotes extends ApiClient { | ||||
|   constructor() { | ||||
|     super('contact_notes', { accountScoped: true }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default new ContactNotes(); | ||||
| @@ -8,6 +8,7 @@ | ||||
| } | ||||
|  | ||||
| .card { | ||||
|   margin-bottom: var(--space-small); | ||||
|   padding: var(--space-small); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -4,27 +4,28 @@ | ||||
|       <contact-panel v-if="!uiFlags.isFetchingItem" :contact="contact" /> | ||||
|     </div> | ||||
|     <div class="center"></div> | ||||
|     <div class="right"></div> | ||||
|     <div class="right"> | ||||
|       <contact-notes :contact-id="Number(contactId)" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import ContactPanel from './ContactPanel'; | ||||
| import ContactNotes from 'dashboard/modules/notes/NotesOnContactPage'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     ContactPanel, | ||||
|     ContactNotes, | ||||
|   }, | ||||
|   props: { | ||||
|     contactId: { | ||||
|       type: [String, Number], | ||||
|       default: 0, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return {}; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       uiFlags: 'contacts/getUIFlags', | ||||
| @@ -38,13 +39,12 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getContactDetails(); | ||||
|     this.fetchContactDetails(); | ||||
|   }, | ||||
|   methods: { | ||||
|     getContactDetails() { | ||||
|       if (this.contactId) { | ||||
|         this.$store.dispatch('contacts/show', { id: this.contactId }); | ||||
|       } | ||||
|     fetchContactDetails() { | ||||
|       const { contactId: id } = this; | ||||
|       this.$store.dispatch('contacts/show', { id }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| @@ -67,4 +67,8 @@ export default { | ||||
|   border-right: 1px solid var(--color-border); | ||||
|   border-left: 1px solid var(--color-border); | ||||
| } | ||||
|  | ||||
| .right { | ||||
|   padding: var(--space-normal); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -0,0 +1,43 @@ | ||||
| <template> | ||||
|   <note-list :notes="notes" @add="onAdd" @delete="onDelete" /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import NoteList from './components/NoteList'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     NoteList, | ||||
|   }, | ||||
|   props: { | ||||
|     contactId: { | ||||
|       type: Number, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     notes() { | ||||
|       return this.$store.getters['contactNotes/getAllNotesByContact']( | ||||
|         this.contactId | ||||
|       ); | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.fetchContactNotes(); | ||||
|   }, | ||||
|   methods: { | ||||
|     fetchContactNotes() { | ||||
|       const { contactId } = this; | ||||
|       if (contactId) this.$store.dispatch('contactNotes/get', { contactId }); | ||||
|     }, | ||||
|     onAdd(content) { | ||||
|       const { contactId } = this; | ||||
|       this.$store.dispatch('contactNotes/create', { content, contactId }); | ||||
|     }, | ||||
|     onDelete(noteId) { | ||||
|       const { contactId } = this; | ||||
|       this.$store.dispatch('contactNotes/delete', { noteId, contactId }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -17,7 +17,7 @@ | ||||
|         @delete="onDeleteNote" | ||||
|       /> | ||||
|       <div class="button-wrap"> | ||||
|         <woot-button variant="clear link" class="button" @click="onclick"> | ||||
|         <woot-button variant="link" @click="onclick"> | ||||
|           {{ $t('NOTES.FOOTER.BUTTON') }} | ||||
|           <i class="ion-arrow-right-c" /> | ||||
|         </woot-button> | ||||
| @@ -48,13 +48,13 @@ export default { | ||||
|       this.$emit('show'); | ||||
|     }, | ||||
|     onAddNote(value) { | ||||
|       this.$emit('addNote', value); | ||||
|       this.$emit('add', value); | ||||
|     }, | ||||
|     onEditNote(value) { | ||||
|       this.$emit('editNote', value); | ||||
|       this.$emit('edit', value); | ||||
|     }, | ||||
|     onDeleteNote(value) { | ||||
|       this.$emit('deleteNote', value); | ||||
|       this.$emit('delete', value); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| import AddNote from './AddNote.vue'; | ||||
| import AddNote from '../components/AddNote.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   title: 'Components/Notes/Add', | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| import ContactNote from './ContactNote.vue'; | ||||
| import ContactNote from '../components/ContactNote.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   title: 'Components/Notes/Note', | ||||
| @@ -1,15 +1,15 @@ | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| import noteList from './NoteList'; | ||||
| import NoteList from '../components/NoteList'; | ||||
| 
 | ||||
| export default { | ||||
|   title: 'Components/Notes/List', | ||||
|   component: noteList, | ||||
|   component: NoteList, | ||||
|   argTypes: {}, | ||||
| }; | ||||
| 
 | ||||
| const Template = (args, { argTypes }) => ({ | ||||
|   props: Object.keys(argTypes), | ||||
|   components: { noteList }, | ||||
|   components: { NoteList }, | ||||
|   template: | ||||
|     '<note-list v-bind="$props" @addNote="onAddNote" @editNote="onEditNote" @deleteNote="onDeleteNote" @show="onClick"></note-list>', | ||||
| }); | ||||
| @@ -27,6 +27,7 @@ import webhooks from './modules/webhooks'; | ||||
| import teams from './modules/teams'; | ||||
| import teamMembers from './modules/teamMembers'; | ||||
| import campaigns from './modules/campaigns'; | ||||
| import contactNotes from './modules/contactNotes'; | ||||
|  | ||||
| Vue.use(Vuex); | ||||
| export default new Vuex.Store({ | ||||
| @@ -57,5 +58,6 @@ export default new Vuex.Store({ | ||||
|     teams, | ||||
|     teamMembers, | ||||
|     campaigns, | ||||
|     contactNotes, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
							
								
								
									
										107
									
								
								app/javascript/dashboard/store/modules/contactNotes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								app/javascript/dashboard/store/modules/contactNotes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import * as types from '../mutation-types'; | ||||
| import Vue from 'vue'; | ||||
| import ContactNotesAPI from '../../api/contactNotes'; | ||||
|  | ||||
| export const state = { | ||||
|   records: {}, | ||||
|   uiFlags: { | ||||
|     isFetching: false, | ||||
|     isCreating: false, | ||||
|     isDeleting: false, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const getters = { | ||||
|   getAllNotesByContact: _state => contactId => { | ||||
|     return _state.records[contactId] || []; | ||||
|   }, | ||||
|   getUIFlags(_state) { | ||||
|     return _state.uiFlags; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const actions = { | ||||
|   async get({ commit }, { contactId }) { | ||||
|     commit(types.default.SET_CONTACT_NOTES_UI_FLAG, { | ||||
|       isFetching: true, | ||||
|     }); | ||||
|     try { | ||||
|       const { data } = await ContactNotesAPI.get(contactId); | ||||
|       commit(types.default.SET_CONTACT_NOTES, { contactId, data }); | ||||
|     } catch (error) { | ||||
|       throw new Error(error); | ||||
|     } finally { | ||||
|       commit(types.default.SET_CONTACT_NOTES_UI_FLAG, { | ||||
|         isFetching: false, | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   async create({ commit }, { contactId, content }) { | ||||
|     commit(types.default.SET_CONTACT_NOTES_UI_FLAG, { | ||||
|       isCreating: true, | ||||
|     }); | ||||
|     try { | ||||
|       const { data } = await ContactNotesAPI.create({ | ||||
|         content, | ||||
|         contactId, | ||||
|       }); | ||||
|       commit(types.default.ADD_CONTACT_NOTE, { | ||||
|         contactId, | ||||
|         data, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       throw new Error(error); | ||||
|     } finally { | ||||
|       commit(types.default.SET_CONTACT_NOTES_UI_FLAG, { | ||||
|         isCreating: false, | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   async delete({ commit }, { noteId, contactId }) { | ||||
|     commit(types.default.SET_CONTACT_NOTES_UI_FLAG, { | ||||
|       isDeleting: true, | ||||
|     }); | ||||
|     try { | ||||
|       await ContactNotesAPI.delete(contactId, noteId); | ||||
|       commit(types.default.DELETE_CONTACT_NOTE, { contactId, noteId }); | ||||
|     } catch (error) { | ||||
|       throw new Error(error); | ||||
|     } finally { | ||||
|       commit(types.default.SET_CONTACT_NOTES_UI_FLAG, { | ||||
|         isDeleting: false, | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const mutations = { | ||||
|   [types.default.SET_CONTACT_NOTES_UI_FLAG](_state, data) { | ||||
|     _state.uiFlags = { | ||||
|       ..._state.uiFlags, | ||||
|       ...data, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   [types.default.SET_CONTACT_NOTES]($state, { data, contactId }) { | ||||
|     Vue.set($state.records, contactId, data); | ||||
|   }, | ||||
|   [types.default.ADD_CONTACT_NOTE]($state, { data, contactId }) { | ||||
|     const contactNotes = $state.records[contactId] || []; | ||||
|     $state.records[contactId] = [...contactNotes, data]; | ||||
|   }, | ||||
|   [types.default.DELETE_CONTACT_NOTE]($state, { noteId, contactId }) { | ||||
|     const contactNotes = $state.records[contactId]; | ||||
|     const withoutDeletedNote = contactNotes.filter(note => note.id !== noteId); | ||||
|     $state.records[contactId] = [...withoutDeletedNote]; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|   namespaced: true, | ||||
|   state, | ||||
|   getters, | ||||
|   actions, | ||||
|   mutations, | ||||
| }; | ||||
| @@ -0,0 +1,78 @@ | ||||
| import axios from 'axios'; | ||||
| import { actions } from '../../contactNotes'; | ||||
| import * as types from '../../../mutation-types'; | ||||
| import notesData from './fixtures'; | ||||
|  | ||||
| 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: notesData }); | ||||
|       await actions.get({ commit }, { contactId: 23 }); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isFetching: true }], | ||||
|         [types.default.SET_CONTACT_NOTES, { contactId: 23, data: notesData }], | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isFetching: false }], | ||||
|       ]); | ||||
|     }); | ||||
|     it('sends correct actions if API is error', async () => { | ||||
|       axios.get.mockRejectedValue({ message: 'Incorrect header' }); | ||||
|       await expect(actions.get({ commit }, { contactId: 23 })).rejects.toThrow( | ||||
|         Error | ||||
|       ); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isFetching: true }], | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isFetching: false }], | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
|   describe('#create', () => { | ||||
|     it('sends correct actions if API is success', async () => { | ||||
|       axios.post.mockResolvedValue({ data: { id: 2, content: 'hi' } }); | ||||
|       await actions.create({ commit }, { contactId: 1, content: 'hi' }); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isCreating: true }], | ||||
|         [ | ||||
|           types.default.ADD_CONTACT_NOTE, | ||||
|           { contactId: 1, data: { id: 2, content: 'hi' } }, | ||||
|         ], | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isCreating: false }], | ||||
|       ]); | ||||
|     }); | ||||
|     it('sends correct actions if API is error', async () => { | ||||
|       axios.post.mockRejectedValue({ message: 'Incorrect header' }); | ||||
|       await expect( | ||||
|         actions.create({ commit }, { contactId: 1, content: 'hi' }) | ||||
|       ).rejects.toThrow(Error); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isCreating: true }], | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isCreating: false }], | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('#delete', () => { | ||||
|     it('sends correct actions if API is success', async () => { | ||||
|       axios.delete.mockResolvedValue({ data: notesData[0] }); | ||||
|       await actions.delete({ commit }, { contactId: 1, noteId: 2 }); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isDeleting: true }], | ||||
|         [types.default.DELETE_CONTACT_NOTE, { contactId: 1, noteId: 2 }], | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isDeleting: false }], | ||||
|       ]); | ||||
|     }); | ||||
|     it('sends correct actions if API is error', async () => { | ||||
|       axios.delete.mockRejectedValue({ message: 'Incorrect header' }); | ||||
|       await expect( | ||||
|         actions.delete({ commit }, { contactId: 1, noteId: 2 }) | ||||
|       ).rejects.toThrow(Error); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isDeleting: true }], | ||||
|         [types.default.SET_CONTACT_NOTES_UI_FLAG, { isDeleting: false }], | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,21 @@ | ||||
| export default [ | ||||
|   { | ||||
|     id: 12345, | ||||
|     content: 'It is a long established fact that a reader will be distracted.', | ||||
|     user: { | ||||
|       name: 'John Doe', | ||||
|       thumbnail: 'https://randomuser.me/api/portraits/men/69.jpg', | ||||
|     }, | ||||
|     created_at: 1618046084, | ||||
|   }, | ||||
|   { | ||||
|     id: 12346, | ||||
|     content: | ||||
|       'It is simply dummy text of the printing and typesetting industry.', | ||||
|     user: { | ||||
|       name: 'Pearl Cruz', | ||||
|       thumbnail: 'https://randomuser.me/api/portraits/women/29.jpg', | ||||
|     }, | ||||
|     created_at: 1616046076, | ||||
|   }, | ||||
| ]; | ||||
| @@ -0,0 +1,24 @@ | ||||
| import { getters } from '../../contactNotes'; | ||||
| import notesData from './fixtures'; | ||||
|  | ||||
| describe('#getters', () => { | ||||
|   it('getAllNotesByContact', () => { | ||||
|     const state = { records: { 1: notesData } }; | ||||
|     expect(getters.getAllNotesByContact(state)(1)).toEqual(notesData); | ||||
|   }); | ||||
|  | ||||
|   it('getUIFlags', () => { | ||||
|     const state = { | ||||
|       uiFlags: { | ||||
|         isFetching: true, | ||||
|         isCreating: false, | ||||
|         isDeleting: false, | ||||
|       }, | ||||
|     }; | ||||
|     expect(getters.getUIFlags(state)).toEqual({ | ||||
|       isFetching: true, | ||||
|       isCreating: false, | ||||
|       isDeleting: false, | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,37 @@ | ||||
| import types from '../../../mutation-types'; | ||||
| import { mutations } from '../../contactNotes'; | ||||
| import allNotes from './fixtures'; | ||||
|  | ||||
| describe('#mutations', () => { | ||||
|   describe('#SET_CONTACT_NOTES', () => { | ||||
|     it('set allNotes records', () => { | ||||
|       const state = { records: {} }; | ||||
|       mutations[types.SET_CONTACT_NOTES](state, { | ||||
|         data: allNotes, | ||||
|         contactId: 1, | ||||
|       }); | ||||
|       expect(state.records).toEqual({ 1: allNotes }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('#ADD_CONTACT_NOTE', () => { | ||||
|     it('push newly created note to the store', () => { | ||||
|       const state = { records: { 1: [allNotes[0]] } }; | ||||
|       mutations[types.ADD_CONTACT_NOTE](state, { | ||||
|         data: allNotes[1], | ||||
|         contactId: 1, | ||||
|       }); | ||||
|       expect(state.records[1]).toEqual([allNotes[0], allNotes[1]]); | ||||
|     }); | ||||
|   }); | ||||
|   describe('#DELETE_CONTACT_NOTE', () => { | ||||
|     it('Delete existing note from records', () => { | ||||
|       const state = { records: { 1: [{ id: 2 }] } }; | ||||
|       mutations[types.DELETE_CONTACT_NOTE](state, { | ||||
|         noteId: 2, | ||||
|         contactId: 1, | ||||
|       }); | ||||
|       expect(state.records[1]).toEqual([]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -154,4 +154,11 @@ export default { | ||||
|   SET_CAMPAIGNS: 'SET_CAMPAIGNS', | ||||
|   ADD_CAMPAIGN: 'ADD_CAMPAIGN', | ||||
|   EDIT_CAMPAIGN: 'EDIT_CAMPAIGN', | ||||
|  | ||||
|   // Contact notes | ||||
|   SET_CONTACT_NOTES_UI_FLAG: 'SET_CONTACT_NOTES_UI_FLAG', | ||||
|   SET_CONTACT_NOTES: 'SET_CONTACT_NOTES', | ||||
|   ADD_CONTACT_NOTE: 'ADD_CONTACT_NOTE', | ||||
|   EDIT_CONTACT_NOTE: 'EDIT_CONTACT_NOTE', | ||||
|   DELETE_CONTACT_NOTE: 'DELETE_CONTACT_NOTE', | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user