Feat: Create contact from contacts page (#1806)

* Add contact create modal to contacts page

* Test cases

* Review fixes
This commit is contained in:
Nithin David Thomas 2021-02-19 20:22:58 +05:30 committed by GitHub
parent 6ba25bae3d
commit c17380d48a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 187 additions and 11 deletions

View file

@ -45,6 +45,11 @@
"TITLE": "Edit contact", "TITLE": "Edit contact",
"DESC": "Edit contact details" "DESC": "Edit contact details"
}, },
"CREATE_CONTACT": {
"BUTTON_LABEL": "New Contact",
"TITLE": "Create new contact",
"DESC": "Add basic information details about the contact."
},
"CONTACT_FORM": { "CONTACT_FORM": {
"FORM": { "FORM": {
"SUBMIT": "Submit", "SUBMIT": "Submit",
@ -95,9 +100,9 @@
} }
} }
}, },
"SUCCESS_MESSAGE": "Updated contact successfully", "SUCCESS_MESSAGE": "Contact saved successfully",
"CONTACT_ALREADY_EXIST": "This email address is in use for another contact.", "CONTACT_ALREADY_EXIST": "This email address is in use for another contact.",
"ERROR_MESSAGE": "There was an error updating the contact, please try again" "ERROR_MESSAGE": "There was an error, please try again"
}, },
"CONTACTS_PAGE": { "CONTACTS_PAGE": {
"HEADER": "Contacts", "HEADER": "Contacts",

View file

@ -6,6 +6,7 @@
:on-search-submit="onSearchSubmit" :on-search-submit="onSearchSubmit"
this-selected-contact-id="" this-selected-contact-id=""
:on-input-search="onInputSearch" :on-input-search="onInputSearch"
:on-toggle-create="onToggleCreate"
/> />
<contacts-table <contacts-table
:contacts="records" :contacts="records"
@ -25,6 +26,7 @@
:contact="selectedContact" :contact="selectedContact"
:on-close="closeContactInfoPanel" :on-close="closeContactInfoPanel"
/> />
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
</div> </div>
</template> </template>
@ -34,6 +36,7 @@ import { mapGetters } from 'vuex';
import ContactsHeader from './Header'; import ContactsHeader from './Header';
import ContactsTable from './ContactsTable'; import ContactsTable from './ContactsTable';
import ContactInfoPanel from './ContactInfoPanel'; import ContactInfoPanel from './ContactInfoPanel';
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
import TableFooter from 'dashboard/components/widgets/TableFooter'; import TableFooter from 'dashboard/components/widgets/TableFooter';
export default { export default {
@ -42,11 +45,12 @@ export default {
ContactsTable, ContactsTable,
TableFooter, TableFooter,
ContactInfoPanel, ContactInfoPanel,
CreateContact,
}, },
data() { data() {
return { return {
searchQuery: '', searchQuery: '',
showEditModal: false, showCreateModal: false,
selectedContactId: '', selectedContactId: '',
}; };
}, },
@ -123,6 +127,9 @@ export default {
this.selectedContactId = ''; this.selectedContactId = '';
this.showContactInfoPanelPane = false; this.showContactInfoPanelPane = false;
}, },
onToggleCreate() {
this.showCreateModal = !this.showCreateModal;
},
}, },
}; };
</script> </script>

View file

@ -24,6 +24,11 @@
@click="onSearchSubmit" @click="onSearchSubmit"
/> />
</div> </div>
<button class="button success icon" @click="onToggleCreate">
<i class="icon ion-android-add-circle" />
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
</button>
</div> </div>
</div> </div>
</header> </header>
@ -45,6 +50,15 @@ export default {
type: Function, type: Function,
default: () => {}, default: () => {},
}, },
onToggleCreate: {
type: Function,
default: () => {},
},
},
data() {
return {
showCreateModal: false,
};
}, },
computed: { computed: {
searchButtonClass() { searchButtonClass() {
@ -69,35 +83,41 @@ export default {
width: 100%; width: 100%;
margin-bottom: var(--space-slab); margin-bottom: var(--space-slab);
} }
.right-aligned-wrap {
display: flex;
}
.search-wrap { .search-wrap {
width: 400px; width: 400px;
height: 3.6rem; height: 3.8rem;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative; position: relative;
margin-right: var(--space-small);
.search-icon { .search-icon {
position: absolute; position: absolute;
top: 1px; top: 1px;
left: var(--space-one); left: var(--space-one);
height: 3.6rem; height: 3.8rem;
line-height: 3.6rem; line-height: 3.6rem;
font-size: var(--font-size-medium); font-size: var(--font-size-medium);
color: var(--b-700); color: var(--b-700);
} }
.contact-search { .contact-search {
margin: 0; margin: 0;
height: 3.6rem; height: 3.8rem;
width: 100%; width: 100%;
padding-left: var(--space-large); padding-left: var(--space-large);
padding-right: 6rem; padding-right: 6rem;
border-color: var(--s-100);
} }
.button { .button {
margin-left: var(--space-small); margin-left: var(--space-small);
height: 3.2rem; height: 3.2rem;
top: var(--space-micro); right: var(--space-smaller);
right: var(--space-micro);
position: absolute; position: absolute;
padding: 0 var(--space-small); padding: 0 var(--space-small);
transition: transform 100ms linear; transition: transform 100ms linear;

View file

@ -81,7 +81,10 @@
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import { DuplicateContactException } from 'shared/helpers/CustomErrors'; import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
export default { export default {
@ -144,6 +147,9 @@ export default {
onCancel() { onCancel() {
this.$emit('cancel'); this.$emit('cancel');
}, },
onSuccess() {
this.$emit('success');
},
setContactObject() { setContactObject() {
const { email: email, phone_number: phoneNumber, name } = this.contact; const { email: email, phone_number: phoneNumber, name } = this.contact;
const additionalAttributes = this.contact.additional_attributes || {}; const additionalAttributes = this.contact.additional_attributes || {};
@ -189,12 +195,15 @@ export default {
} }
try { try {
await this.onSubmit(this.getContactObject()); await this.onSubmit(this.getContactObject());
this.onSuccess();
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE')); this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
} catch (error) { } catch (error) {
if (error instanceof DuplicateContactException) { if (error instanceof DuplicateContactException) {
this.hasADuplicateContact = true; this.hasADuplicateContact = true;
this.duplicateContact = error.data; this.duplicateContact = error.data;
this.showAlert(this.$t('CONTACT_FORM.CONTACT_ALREADY_EXIST')); this.showAlert(this.$t('CONTACT_FORM.CONTACT_ALREADY_EXIST'));
} else if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data);
} else { } else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE')); this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
} }
@ -212,4 +221,8 @@ export default {
padding: 0 var(--space-smaller); padding: 0 var(--space-smaller);
} }
} }
.input-group-label {
font-size: var(--font-size-small);
}
</style> </style>

View file

@ -0,0 +1,55 @@
<template>
<woot-modal :show.sync="show" :on-close="onCancel">
<div class="column content-box">
<woot-modal-header
:header-title="$t('CREATE_CONTACT.TITLE')"
:header-content="$t('CREATE_CONTACT.DESC')"
/>
<contact-form
:in-progress="uiFlags.isCreating"
:on-submit="onSubmit"
@success="onSuccess"
@cancel="onCancel"
/>
</div>
</woot-modal>
</template>
<script>
import { mapGetters } from 'vuex';
import ContactForm from './ContactForm';
export default {
components: {
ContactForm,
},
props: {
show: {
type: Boolean,
default: false,
},
contact: {
type: Object,
default: () => ({}),
},
},
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
},
methods: {
onCancel() {
this.$emit('cancel');
},
onSuccess() {
this.$emit('cancel');
},
async onSubmit(contactItem) {
await this.$store.dispatch('contacts/create', contactItem);
},
},
};
</script>

View file

@ -11,6 +11,8 @@
:contact="contact" :contact="contact"
:in-progress="uiFlags.isUpdating" :in-progress="uiFlags.isUpdating"
:on-submit="onSubmit" :on-submit="onSubmit"
@success="onSuccess"
@cancel="onCancel"
/> />
</div> </div>
</woot-modal> </woot-modal>
@ -45,6 +47,9 @@ export default {
onCancel() { onCancel() {
this.$emit('cancel'); this.$emit('cancel');
}, },
onSuccess() {
this.$emit('cancel');
},
async onSubmit(contactItem) { async onSubmit(contactItem) {
await this.$store.dispatch('contacts/update', contactItem); await this.$store.dispatch('contacts/update', contactItem);
}, },

View file

@ -1,4 +1,7 @@
import { DuplicateContactException } from 'shared/helpers/CustomErrors'; import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import types from '../../mutation-types'; import types from '../../mutation-types';
import ContactAPI from '../../../api/contacts'; import ContactAPI from '../../../api/contacts';
@ -64,6 +67,22 @@ export const actions = {
} }
}, },
create: async ({ commit }, userObject) => {
commit(types.SET_CONTACT_UI_FLAG, { isCreating: true });
try {
const response = await ContactAPI.create(userObject);
commit(types.SET_CONTACT_ITEM, response.data.payload.contact);
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
if (error.response?.data?.message) {
throw new ExceptionWithMessage(error.response.data.message);
} else {
throw new Error(error);
}
}
},
updatePresence: ({ commit }, data) => { updatePresence: ({ commit }, data) => {
commit(types.UPDATE_CONTACTS_PRESENCE, data); commit(types.UPDATE_CONTACTS_PRESENCE, data);
}, },

View file

@ -2,7 +2,10 @@ import axios from 'axios';
import Contacts from '../../contacts'; import Contacts from '../../contacts';
import types from '../../../mutation-types'; import types from '../../../mutation-types';
import contactList from './fixtures'; import contactList from './fixtures';
import { DuplicateContactException } from '../../../../../shared/helpers/CustomErrors'; import {
DuplicateContactException,
ExceptionWithMessage,
} from '../../../../../shared/helpers/CustomErrors';
const { actions } = Contacts; const { actions } = Contacts;
@ -95,6 +98,47 @@ describe('#actions', () => {
}); });
}); });
describe('#create', () => {
it('sends correct mutations if API is success', async () => {
axios.post.mockResolvedValue({
data: { payload: { contact: contactList[0] } },
});
await actions.create({ commit }, contactList[0]);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isCreating: true }],
[types.SET_CONTACT_ITEM, contactList[0]],
[types.SET_CONTACT_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.create({ commit }, contactList[0])).rejects.toThrow(
Error
);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isCreating: true }],
[types.SET_CONTACT_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if email is already present', async () => {
axios.post.mockRejectedValue({
response: {
data: {
message: 'Email exists already',
},
},
});
await expect(actions.create({ commit }, contactList[0])).rejects.toThrow(
ExceptionWithMessage
);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isCreating: true }],
[types.SET_CONTACT_UI_FLAG, { isCreating: false }],
]);
});
});
describe('#setContact', () => { describe('#setContact', () => {
it('returns correct mutations', () => { it('returns correct mutations', () => {
const data = { id: 1, name: 'john doe', availability_status: 'online' }; const data = { id: 1, name: 'john doe', availability_status: 'online' };

View file

@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
export class DuplicateContactException extends Error { export class DuplicateContactException extends Error {
constructor(data) { constructor(data) {
super('DUPLICATE_CONTACT'); super('DUPLICATE_CONTACT');
@ -5,3 +6,10 @@ export class DuplicateContactException extends Error {
this.name = 'DuplicateContactException'; this.name = 'DuplicateContactException';
} }
} }
export class ExceptionWithMessage extends Error {
constructor(data) {
super('ERROR_WITH_MESSAGE');
this.data = data;
this.name = 'ExceptionWithMessage';
}
}