feat: Add ability to bulk import contacts (#3026)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Fayaz Ahmed
2021-09-29 12:01:58 +05:30
committed by GitHub
parent 6129edce08
commit bba2750975
10 changed files with 195 additions and 5 deletions

View File

@@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
)}`;
return axios.get(requestURL);
}
importContacts(file) {
const formData = new FormData();
formData.append('import_file', file);
return axios.post(`${this.url}/import`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
}
export default new ContactAPI();

View File

@@ -59,6 +59,18 @@ describe('#ContactsAPI', () => {
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
);
});
it('#importContacts', () => {
const file = 'file';
contactAPI.importContacts(file);
expect(context.axiosMock.post).toHaveBeenCalledWith(
'/api/v1/contacts/import',
expect.any(FormData),
{
headers: { 'Content-Type': 'multipart/form-data' },
}
);
});
});
});

View File

@@ -0,0 +1,3 @@
.margin-right-small {
margin-right: var(--space-small);
}

View File

@@ -71,7 +71,8 @@
@include padding($space-large);
}
form {
form,
.modal-content {
@include padding($space-large);
align-self: center;

View File

@@ -54,6 +54,19 @@
"TITLE": "Create new contact",
"DESC": "Add basic information details about the contact."
},
"IMPORT_CONTACTS": {
"BUTTON_LABEL": "Import",
"TITLE": "Import Contacts",
"DESC": "Import contacts through a CSV file.",
"DOWNLOAD_LABEL": "Download a sample csv.",
"FORM": {
"LABEL": "CSV File",
"SUBMIT": "Import",
"CANCEL": "Cancel"
},
"SUCCESS_MESSAGE": "Contacts saved successfully",
"ERROR_MESSAGE": "There was an error, please try again"
},
"DELETE_CONTACT": {
"BUTTON_LABEL": "Delete Contact",
"TITLE": "Delete contact",
@@ -255,4 +268,4 @@
"ERROR_MESSAGE": "Could not merge contcts, try again!"
}
}
}
}

View File

@@ -7,6 +7,7 @@
this-selected-contact-id=""
:on-input-search="onInputSearch"
:on-toggle-create="onToggleCreate"
:on-toggle-import="onToggleImport"
:header-title="label"
/>
<contacts-table
@@ -30,6 +31,9 @@
:on-close="closeContactInfoPanel"
/>
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
<woot-modal :show.sync="showImportModal" :on-close="onToggleImport">
<import-contacts v-if="showImportModal" :on-close="onToggleImport" />
</woot-modal>
</div>
</template>
@@ -41,6 +45,7 @@ import ContactsTable from './ContactsTable';
import ContactInfoPanel from './ContactInfoPanel';
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
import TableFooter from 'dashboard/components/widgets/TableFooter';
import ImportContacts from './ImportContacts.vue';
const DEFAULT_PAGE = 1;
@@ -51,6 +56,7 @@ export default {
TableFooter,
ContactInfoPanel,
CreateContact,
ImportContacts,
},
props: {
label: { type: String, default: '' },
@@ -59,6 +65,7 @@ export default {
return {
searchQuery: '',
showCreateModal: false,
showImportModal: false,
selectedContactId: '',
sortConfig: { name: 'asc' },
};
@@ -168,6 +175,9 @@ export default {
onToggleCreate() {
this.showCreateModal = !this.showCreateModal;
},
onToggleImport() {
this.showImportModal = !this.showImportModal;
},
onSortChange(params) {
this.sortConfig = params;
this.fetchContacts(this.meta.currentPage);

View File

@@ -29,11 +29,20 @@
<woot-button
color-scheme="success"
icon="ion-android-add-circle"
@click="onToggleCreate"
class="margin-right-small"
data-testid="create-new-contact"
@click="onToggleCreate"
>
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
</woot-button>
<woot-button
color-scheme="info"
icon="ion-android-upload"
@click="onToggleImport"
>
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</header>
@@ -41,7 +50,6 @@
<script>
export default {
components: {},
props: {
headerTitle: {
type: String,
@@ -63,10 +71,15 @@ export default {
type: Function,
default: () => {},
},
onToggleImport: {
type: Function,
default: () => {},
},
},
data() {
return {
showCreateModal: false,
showImportModal: false,
};
},
computed: {
@@ -78,6 +91,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/_utility-helpers.scss';
.page-title {
margin: 0;
}

View File

@@ -0,0 +1,92 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<div class="column content-box">
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
<p>
{{ $t('IMPORT_CONTACTS.DESC') }}
<a :href="csvUrl" download="import-contacts-sample">{{
$t('IMPORT_CONTACTS.DOWNLOAD_LABEL')
}}</a>
</p>
</woot-modal-header>
<div class="row modal-content">
<div class="medium-12 columns">
<label>
<span>{{ $t('IMPORT_CONTACTS.FORM.LABEL') }}</span>
<input
id="file"
ref="file"
type="file"
accept="text/csv"
@change="handleFileUpload"
/>
</label>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-button
:disabled="uiFlags.isCreating || !file"
:loading="uiFlags.isCreating"
@click="uploadFile"
>
{{ $t('IMPORT_CONTACTS.FORM.SUBMIT') }}
</woot-button>
<button class="button clear" @click.prevent="onClose">
{{ $t('IMPORT_CONTACTS.FORM.CANCEL') }}
</button>
</div>
</div>
</div>
</div>
</modal>
</template>
<script>
import Modal from '../../../../components/Modal';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Modal,
},
mixins: [alertMixin],
props: {
onClose: {
type: Function,
default: () => {},
},
},
data() {
return {
show: true,
file: '',
};
},
computed: {
...mapGetters({
uiFlags: 'contacts/getUIFlags',
}),
csvUrl() {
return '/downloads/import-contacts-sample.csv';
},
},
methods: {
async uploadFile() {
try {
if (!this.file) return;
await this.$store.dispatch('contacts/import', this.file);
this.onClose();
this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(
error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE')
);
}
},
handleFileUpload() {
this.file = this.$refs.file.files[0];
},
},
};
</script>

View File

@@ -82,7 +82,18 @@ export const actions = {
}
}
},
import: async ({ commit }, file) => {
commit(types.SET_CONTACT_UI_FLAG, { isCreating: true });
try {
await ContactAPI.importContacts(file);
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);
}
}
},
delete: async ({ commit }, id) => {
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
try {