mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	Feat: Create contact from contacts page (#1806)
* Add contact create modal to contacts page * Test cases * Review fixes
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							6ba25bae3d
						
					
				
				
					commit
					c17380d48a
				
			@@ -45,6 +45,11 @@
 | 
			
		||||
    "TITLE": "Edit contact",
 | 
			
		||||
    "DESC": "Edit contact details"
 | 
			
		||||
  },
 | 
			
		||||
  "CREATE_CONTACT": {
 | 
			
		||||
    "BUTTON_LABEL": "New Contact",
 | 
			
		||||
    "TITLE": "Create new contact",
 | 
			
		||||
    "DESC": "Add basic information details about the contact."
 | 
			
		||||
  },
 | 
			
		||||
  "CONTACT_FORM": {
 | 
			
		||||
    "FORM": {
 | 
			
		||||
      "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.",
 | 
			
		||||
    "ERROR_MESSAGE": "There was an error updating the contact, please try again"
 | 
			
		||||
    "ERROR_MESSAGE": "There was an error, please try again"
 | 
			
		||||
  },
 | 
			
		||||
  "CONTACTS_PAGE": {
 | 
			
		||||
    "HEADER": "Contacts",
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
        :on-search-submit="onSearchSubmit"
 | 
			
		||||
        this-selected-contact-id=""
 | 
			
		||||
        :on-input-search="onInputSearch"
 | 
			
		||||
        :on-toggle-create="onToggleCreate"
 | 
			
		||||
      />
 | 
			
		||||
      <contacts-table
 | 
			
		||||
        :contacts="records"
 | 
			
		||||
@@ -25,6 +26,7 @@
 | 
			
		||||
      :contact="selectedContact"
 | 
			
		||||
      :on-close="closeContactInfoPanel"
 | 
			
		||||
    />
 | 
			
		||||
    <create-contact :show="showCreateModal" @cancel="onToggleCreate" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -34,6 +36,7 @@ import { mapGetters } from 'vuex';
 | 
			
		||||
import ContactsHeader from './Header';
 | 
			
		||||
import ContactsTable from './ContactsTable';
 | 
			
		||||
import ContactInfoPanel from './ContactInfoPanel';
 | 
			
		||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
 | 
			
		||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
@@ -42,11 +45,12 @@ export default {
 | 
			
		||||
    ContactsTable,
 | 
			
		||||
    TableFooter,
 | 
			
		||||
    ContactInfoPanel,
 | 
			
		||||
    CreateContact,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      searchQuery: '',
 | 
			
		||||
      showEditModal: false,
 | 
			
		||||
      showCreateModal: false,
 | 
			
		||||
      selectedContactId: '',
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
@@ -123,6 +127,9 @@ export default {
 | 
			
		||||
      this.selectedContactId = '';
 | 
			
		||||
      this.showContactInfoPanelPane = false;
 | 
			
		||||
    },
 | 
			
		||||
    onToggleCreate() {
 | 
			
		||||
      this.showCreateModal = !this.showCreateModal;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,11 @@
 | 
			
		||||
            @click="onSearchSubmit"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <button class="button success icon" @click="onToggleCreate">
 | 
			
		||||
          <i class="icon ion-android-add-circle" />
 | 
			
		||||
          {{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </header>
 | 
			
		||||
@@ -45,6 +50,15 @@ export default {
 | 
			
		||||
      type: Function,
 | 
			
		||||
      default: () => {},
 | 
			
		||||
    },
 | 
			
		||||
    onToggleCreate: {
 | 
			
		||||
      type: Function,
 | 
			
		||||
      default: () => {},
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showCreateModal: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    searchButtonClass() {
 | 
			
		||||
@@ -69,35 +83,41 @@ export default {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin-bottom: var(--space-slab);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.right-aligned-wrap {
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.search-wrap {
 | 
			
		||||
  width: 400px;
 | 
			
		||||
  height: 3.6rem;
 | 
			
		||||
  height: 3.8rem;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  margin-right: var(--space-small);
 | 
			
		||||
 | 
			
		||||
  .search-icon {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    left: var(--space-one);
 | 
			
		||||
    height: 3.6rem;
 | 
			
		||||
    height: 3.8rem;
 | 
			
		||||
    line-height: 3.6rem;
 | 
			
		||||
    font-size: var(--font-size-medium);
 | 
			
		||||
    color: var(--b-700);
 | 
			
		||||
  }
 | 
			
		||||
  .contact-search {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    height: 3.6rem;
 | 
			
		||||
    height: 3.8rem;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding-left: var(--space-large);
 | 
			
		||||
    padding-right: 6rem;
 | 
			
		||||
    border-color: var(--s-100);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .button {
 | 
			
		||||
    margin-left: var(--space-small);
 | 
			
		||||
    height: 3.2rem;
 | 
			
		||||
    top: var(--space-micro);
 | 
			
		||||
    right: var(--space-micro);
 | 
			
		||||
    right: var(--space-smaller);
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    padding: 0 var(--space-small);
 | 
			
		||||
    transition: transform 100ms linear;
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,10 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
@@ -144,6 +147,9 @@ export default {
 | 
			
		||||
    onCancel() {
 | 
			
		||||
      this.$emit('cancel');
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess() {
 | 
			
		||||
      this.$emit('success');
 | 
			
		||||
    },
 | 
			
		||||
    setContactObject() {
 | 
			
		||||
      const { email: email, phone_number: phoneNumber, name } = this.contact;
 | 
			
		||||
      const additionalAttributes = this.contact.additional_attributes || {};
 | 
			
		||||
@@ -189,12 +195,15 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
      try {
 | 
			
		||||
        await this.onSubmit(this.getContactObject());
 | 
			
		||||
        this.onSuccess();
 | 
			
		||||
        this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error instanceof DuplicateContactException) {
 | 
			
		||||
          this.hasADuplicateContact = true;
 | 
			
		||||
          this.duplicateContact = error.data;
 | 
			
		||||
          this.showAlert(this.$t('CONTACT_FORM.CONTACT_ALREADY_EXIST'));
 | 
			
		||||
        } else if (error instanceof ExceptionWithMessage) {
 | 
			
		||||
          this.showAlert(error.data);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
 | 
			
		||||
        }
 | 
			
		||||
@@ -212,4 +221,8 @@ export default {
 | 
			
		||||
    padding: 0 var(--space-smaller);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.input-group-label {
 | 
			
		||||
  font-size: var(--font-size-small);
 | 
			
		||||
}
 | 
			
		||||
</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"
 | 
			
		||||
        :in-progress="uiFlags.isUpdating"
 | 
			
		||||
        :on-submit="onSubmit"
 | 
			
		||||
        @success="onSuccess"
 | 
			
		||||
        @cancel="onCancel"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </woot-modal>
 | 
			
		||||
@@ -45,6 +47,9 @@ export default {
 | 
			
		||||
    onCancel() {
 | 
			
		||||
      this.$emit('cancel');
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess() {
 | 
			
		||||
      this.$emit('cancel');
 | 
			
		||||
    },
 | 
			
		||||
    async onSubmit(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 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) => {
 | 
			
		||||
    commit(types.UPDATE_CONTACTS_PRESENCE, data);
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,10 @@ import axios from 'axios';
 | 
			
		||||
import Contacts from '../../contacts';
 | 
			
		||||
import types from '../../../mutation-types';
 | 
			
		||||
import contactList from './fixtures';
 | 
			
		||||
import { DuplicateContactException } from '../../../../../shared/helpers/CustomErrors';
 | 
			
		||||
import {
 | 
			
		||||
  DuplicateContactException,
 | 
			
		||||
  ExceptionWithMessage,
 | 
			
		||||
} from '../../../../../shared/helpers/CustomErrors';
 | 
			
		||||
 | 
			
		||||
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', () => {
 | 
			
		||||
    it('returns correct mutations', () => {
 | 
			
		||||
      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 {
 | 
			
		||||
  constructor(data) {
 | 
			
		||||
    super('DUPLICATE_CONTACT');
 | 
			
		||||
@@ -5,3 +6,10 @@ export class DuplicateContactException extends Error {
 | 
			
		||||
    this.name = 'DuplicateContactException';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
export class ExceptionWithMessage extends Error {
 | 
			
		||||
  constructor(data) {
 | 
			
		||||
    super('ERROR_WITH_MESSAGE');
 | 
			
		||||
    this.data = data;
 | 
			
		||||
    this.name = 'ExceptionWithMessage';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user