diff --git a/app/javascript/dashboard/api/contactNotes.js b/app/javascript/dashboard/api/contactNotes.js new file mode 100644 index 000000000..9508ea9dc --- /dev/null +++ b/app/javascript/dashboard/api/contactNotes.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class ContactNotes extends ApiClient { + constructor() { + super('contact_notes', { accountScoped: true }); + } +} + +export default new ContactNotes(); diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index d8909a181..6fc3961fc 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -8,6 +8,7 @@ } .card { + margin-bottom: var(--space-small); padding: var(--space-small); } diff --git a/app/javascript/dashboard/modules/contact/components/ManageLayout.vue b/app/javascript/dashboard/modules/contact/components/ManageLayout.vue index 363ed19df..239a190f1 100644 --- a/app/javascript/dashboard/modules/contact/components/ManageLayout.vue +++ b/app/javascript/dashboard/modules/contact/components/ManageLayout.vue @@ -4,27 +4,28 @@
-
+
+ +
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/AddNote.vue b/app/javascript/dashboard/modules/notes/components/AddNote.vue similarity index 100% rename from app/javascript/dashboard/routes/dashboard/contacts/components/AddNote.vue rename to app/javascript/dashboard/modules/notes/components/AddNote.vue diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactNote.vue b/app/javascript/dashboard/modules/notes/components/ContactNote.vue similarity index 100% rename from app/javascript/dashboard/routes/dashboard/contacts/components/ContactNote.vue rename to app/javascript/dashboard/modules/notes/components/ContactNote.vue diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/NoteList.vue b/app/javascript/dashboard/modules/notes/components/NoteList.vue similarity index 86% rename from app/javascript/dashboard/routes/dashboard/contacts/components/NoteList.vue rename to app/javascript/dashboard/modules/notes/components/NoteList.vue index 4b6df9d6c..656ee3a35 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/NoteList.vue +++ b/app/javascript/dashboard/modules/notes/components/NoteList.vue @@ -17,7 +17,7 @@ @delete="onDeleteNote" />
- + {{ $t('NOTES.FOOTER.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); }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/AddNote.stories.js b/app/javascript/dashboard/modules/notes/stories/AddNote.stories.js similarity index 89% rename from app/javascript/dashboard/routes/dashboard/contacts/components/AddNote.stories.js rename to app/javascript/dashboard/modules/notes/stories/AddNote.stories.js index 6dc059167..1f547d59e 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/AddNote.stories.js +++ b/app/javascript/dashboard/modules/notes/stories/AddNote.stories.js @@ -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', diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactNote.stories.js b/app/javascript/dashboard/modules/notes/stories/ContactNote.stories.js similarity index 94% rename from app/javascript/dashboard/routes/dashboard/contacts/components/ContactNote.stories.js rename to app/javascript/dashboard/modules/notes/stories/ContactNote.stories.js index 36d97af1a..71c3abd81 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactNote.stories.js +++ b/app/javascript/dashboard/modules/notes/stories/ContactNote.stories.js @@ -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', diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/NoteList.stories.js b/app/javascript/dashboard/modules/notes/stories/NoteList.stories.js similarity index 91% rename from app/javascript/dashboard/routes/dashboard/contacts/components/NoteList.stories.js rename to app/javascript/dashboard/modules/notes/stories/NoteList.stories.js index 7f2797bed..c9b8d6345 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/NoteList.stories.js +++ b/app/javascript/dashboard/modules/notes/stories/NoteList.stories.js @@ -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: '', }); diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 09718c41b..867dcecf8 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -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, }, }); diff --git a/app/javascript/dashboard/store/modules/contactNotes.js b/app/javascript/dashboard/store/modules/contactNotes.js new file mode 100644 index 000000000..da4555e0d --- /dev/null +++ b/app/javascript/dashboard/store/modules/contactNotes.js @@ -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, +}; diff --git a/app/javascript/dashboard/store/modules/specs/contactNotes/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contactNotes/actions.spec.js new file mode 100644 index 000000000..166d723c2 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactNotes/actions.spec.js @@ -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 }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/contactNotes/fixtures.js b/app/javascript/dashboard/store/modules/specs/contactNotes/fixtures.js new file mode 100644 index 000000000..ec895621f --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactNotes/fixtures.js @@ -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, + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/contactNotes/getters.spec.js b/app/javascript/dashboard/store/modules/specs/contactNotes/getters.spec.js new file mode 100644 index 000000000..f0eb7e3ce --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactNotes/getters.spec.js @@ -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, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/contactNotes/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/contactNotes/mutations.spec.js new file mode 100644 index 000000000..27c54d79b --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contactNotes/mutations.spec.js @@ -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([]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 152959cb2..f2d101302 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -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', };