Feat: Create contact from contacts page (#1806)
* Add contact create modal to contacts page * Test cases * Review fixes
This commit is contained in:
parent
6ba25bae3d
commit
c17380d48a
9 changed files with 187 additions and 11 deletions
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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' };
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue