feat: Add ability to bulk import contacts (#3026)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
6129edce08
commit
bba2750975
10 changed files with 195 additions and 5 deletions
|
@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
|
||||||
)}`;
|
)}`;
|
||||||
return axios.get(requestURL);
|
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();
|
export default new ContactAPI();
|
||||||
|
|
|
@ -59,6 +59,18 @@ describe('#ContactsAPI', () => {
|
||||||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
'/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' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.margin-right-small {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
|
@ -71,7 +71,8 @@
|
||||||
@include padding($space-large);
|
@include padding($space-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form,
|
||||||
|
.modal-content {
|
||||||
@include padding($space-large);
|
@include padding($space-large);
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,19 @@
|
||||||
"TITLE": "Create new contact",
|
"TITLE": "Create new contact",
|
||||||
"DESC": "Add basic information details about the 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": {
|
"DELETE_CONTACT": {
|
||||||
"BUTTON_LABEL": "Delete Contact",
|
"BUTTON_LABEL": "Delete Contact",
|
||||||
"TITLE": "Delete contact",
|
"TITLE": "Delete contact",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
this-selected-contact-id=""
|
this-selected-contact-id=""
|
||||||
:on-input-search="onInputSearch"
|
:on-input-search="onInputSearch"
|
||||||
:on-toggle-create="onToggleCreate"
|
:on-toggle-create="onToggleCreate"
|
||||||
|
:on-toggle-import="onToggleImport"
|
||||||
:header-title="label"
|
:header-title="label"
|
||||||
/>
|
/>
|
||||||
<contacts-table
|
<contacts-table
|
||||||
|
@ -30,6 +31,9 @@
|
||||||
:on-close="closeContactInfoPanel"
|
:on-close="closeContactInfoPanel"
|
||||||
/>
|
/>
|
||||||
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -41,6 +45,7 @@ import ContactsTable from './ContactsTable';
|
||||||
import ContactInfoPanel from './ContactInfoPanel';
|
import ContactInfoPanel from './ContactInfoPanel';
|
||||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
||||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||||
|
import ImportContacts from './ImportContacts.vue';
|
||||||
|
|
||||||
const DEFAULT_PAGE = 1;
|
const DEFAULT_PAGE = 1;
|
||||||
|
|
||||||
|
@ -51,6 +56,7 @@ export default {
|
||||||
TableFooter,
|
TableFooter,
|
||||||
ContactInfoPanel,
|
ContactInfoPanel,
|
||||||
CreateContact,
|
CreateContact,
|
||||||
|
ImportContacts,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
label: { type: String, default: '' },
|
label: { type: String, default: '' },
|
||||||
|
@ -59,6 +65,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
showCreateModal: false,
|
showCreateModal: false,
|
||||||
|
showImportModal: false,
|
||||||
selectedContactId: '',
|
selectedContactId: '',
|
||||||
sortConfig: { name: 'asc' },
|
sortConfig: { name: 'asc' },
|
||||||
};
|
};
|
||||||
|
@ -168,6 +175,9 @@ export default {
|
||||||
onToggleCreate() {
|
onToggleCreate() {
|
||||||
this.showCreateModal = !this.showCreateModal;
|
this.showCreateModal = !this.showCreateModal;
|
||||||
},
|
},
|
||||||
|
onToggleImport() {
|
||||||
|
this.showImportModal = !this.showImportModal;
|
||||||
|
},
|
||||||
onSortChange(params) {
|
onSortChange(params) {
|
||||||
this.sortConfig = params;
|
this.sortConfig = params;
|
||||||
this.fetchContacts(this.meta.currentPage);
|
this.fetchContacts(this.meta.currentPage);
|
||||||
|
|
|
@ -29,11 +29,20 @@
|
||||||
<woot-button
|
<woot-button
|
||||||
color-scheme="success"
|
color-scheme="success"
|
||||||
icon="ion-android-add-circle"
|
icon="ion-android-add-circle"
|
||||||
@click="onToggleCreate"
|
class="margin-right-small"
|
||||||
data-testid="create-new-contact"
|
data-testid="create-new-contact"
|
||||||
|
@click="onToggleCreate"
|
||||||
>
|
>
|
||||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
|
|
||||||
|
<woot-button
|
||||||
|
color-scheme="info"
|
||||||
|
icon="ion-android-upload"
|
||||||
|
@click="onToggleImport"
|
||||||
|
>
|
||||||
|
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||||
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -41,7 +50,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
|
||||||
props: {
|
props: {
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -63,10 +71,15 @@ export default {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
onToggleImport: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showCreateModal: false,
|
showCreateModal: false,
|
||||||
|
showImportModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -78,6 +91,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/_utility-helpers.scss';
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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) => {
|
delete: async ({ commit }, id) => {
|
||||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
|
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
|
||||||
try {
|
try {
|
||||||
|
|
26
public/downloads/import-contacts-sample.csv
Normal file
26
public/downloads/import-contacts-sample.csv
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
id,name,email,identifier,phone_number,ip_address,custom_attribute_1,custom_attribute_2
|
||||||
|
1,Clarice Uzzell,cuzzell0@mozilla.org,bb4e11cd-0f23-49da-a123-dcc1fec6852c,+498963648018,70.61.11.201,Random-value-1,Random-value-1
|
||||||
|
2,Marieann Creegan,mcreegan1@cornell.edu,e60bab4c-9fbb-47eb-8f75-42025b789c47,+15417543010,168.186.4.241,Random-value0,Random-value0
|
||||||
|
3,Nancey Windibank,nwindibank2@bluehost.com,f793e813-4210-4bf3-a812-711418de25d2,+15417543011,73.44.41.59,Random-value1,Random-value1
|
||||||
|
4,Sibel Stennine,sstennine3@yellowbook.com,d6e35a2d-d093-4437-a577-7df76316b937,+15417543011,115.249.27.155,Random-value2,Random-value2
|
||||||
|
5,Tina O'Lunney,tolunney4@si.edu,3540d40a-5567-4f28-af98-5583a7ddbc56,+15417543011,219.181.212.8,Random-value3,Random-value3
|
||||||
|
6,Quinn Neve,qneve5@army.mil,ba0e1bf0-c74b-41ce-8a2d-0b08fa0e5aa5,+15417543011,231.210.115.166,Random-value4,Random-value4
|
||||||
|
7,Karylin Gaunson,kgaunson6@tripod.com,d24cac79-c81b-4b84-a33e-0441b7c6a981,+15417543011,160.189.41.11,Random-value5,Random-value5
|
||||||
|
8,Jamison Shenton,jshenton7@upenn.edu,29a7a8c0-c7f7-4af9-852f-761b1a784a7a,+15417543011,53.94.18.201,Random-value6,Random-value6
|
||||||
|
9,Gavan Threlfall,gthrelfall8@spotify.com,847d4943-ddb5-47cc-8008-ed5092c675c5,+15417543011,18.87.247.249,Random-value7,Random-value7
|
||||||
|
10,Katina Hemmingway,khemmingway9@ameblo.jp,8f0b5efd-b6a8-4f1e-a1e3-b0ea8c9e3048,+15417543011,25.191.96.124,Random-value8,Random-value8
|
||||||
|
11,Jillian Deinhard,jdeinharda@canalblog.com,bd952787-1b05-411f-9975-b916ec0950cc,+15417543011,11.211.174.93,Random-value9,Random-value9
|
||||||
|
12,Blake Finden,bfindenb@wsj.com,12c95613-e49d-4fa2-86fb-deabb6ebe600,+15417543011,47.26.205.153,Random-value10,Random-value10
|
||||||
|
13,Liane Maxworthy,lmaxworthyc@un.org,36b68e4c-40d6-4e09-bf59-7db3b27b18f0,+15417543011,157.196.34.166,Random-value11,Random-value11
|
||||||
|
14,Martynne Ledley,mledleyd@sourceforge.net,1856bceb-cb36-415c-8ffc-0527f3f750d8,+15417543011,109.231.152.148,Random-value12,Random-value12
|
||||||
|
15,Katharina Ruffli,krufflie@huffingtonpost.com,604de5c9-b154-4279-8978-41fb71f0f773,+15417543011,20.43.146.179,Random-value13,Random-value13
|
||||||
|
16,Tucker Simmance,tsimmancef@bbc.co.uk,0a8fc3a7-4986-4a51-a503-6c7f974c90ad,+15417543011,179.76.226.171,Random-value14,Random-value14
|
||||||
|
17,Wenona Martinson,wmartinsong@census.gov,0e5ea6e3-6824-4e78-a6f5-672847eafa17,+15417543011,92.243.194.160,Random-value15,Random-value15
|
||||||
|
18,Gretna Vedyasov,gvedyasovh@lycos.com,6becf55b-a7b5-48f6-8788-b89cae85b066,+15417543011,25.22.86.101,Random-value16,Random-value16
|
||||||
|
19,Lurline Abdon,labdoni@archive.org,afa9429f-9034-4b06-9efa-980e01906ebf,+15417543011,150.249.116.118,Random-value17,Random-value17
|
||||||
|
20,Fiann Norcliff,fnorcliffj@istockphoto.com,59f72dec-14ba-4d6e-b17c-0d962e69ffac,+15417543011,237.167.197.197,Random-value18,Random-value18
|
||||||
|
21,Zed Linn,zlinnk@phoca.cz,95f7bc56-be92-4c9c-ad58-eff3e63c7bea,+15417543011,88.102.64.113,Random-value19,Random-value19
|
||||||
|
22,Averyl Simyson,asimysonl@livejournal.com,bde1fe59-c9bd-440c-bb39-79fe61dac1d1,+15417543011,141.248.89.29,Random-value20,Random-value20
|
||||||
|
23,Camella Blackadder,cblackadderm@nifty.com,0c981752-5857-487c-b9b5-5d0253df740a,+15417543011,118.123.138.115,Random-value21,Random-value21
|
||||||
|
24,Aurie Spatig,aspatign@printfriendly.com,4cf22bfb-2c3f-41d1-9993-6e3758e457ba,+15417543011,157.45.102.235,Random-value22,Random-value22
|
||||||
|
25,Adrienne Bellard,abellardo@cnn.com,f10f9b8d-38ac-4e17-8a7d-d2e6a055f944,+15417543011,170.73.198.47,Random-value23,Random-value23
|
|
Loading…
Reference in a new issue