Merge branch 'develop' into chore/chat-list-design
This commit is contained in:
commit
034701db8f
35 changed files with 748 additions and 72 deletions
|
@ -41,5 +41,6 @@ exclude_patterns:
|
|||
- "**/*.stories.js"
|
||||
- "stories/"
|
||||
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js"
|
||||
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/countries.js"
|
||||
- "app/javascript/shared/constants/countries.js"
|
||||
- "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js"
|
||||
- "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js"
|
||||
|
|
|
@ -81,11 +81,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
def update
|
||||
@contact.assign_attributes(contact_update_params)
|
||||
@contact.save!
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: {
|
||||
message: e.record.errors.full_messages.join(', '),
|
||||
contact: Current.account.contacts.find_by(email: contact_params[:email])
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
@ -37,7 +37,8 @@ module RequestExceptionHandler
|
|||
|
||||
def render_record_invalid(exception)
|
||||
render json: {
|
||||
message: exception.record.errors.full_messages.join(', ')
|
||||
message: exception.record.errors.full_messages.join(', '),
|
||||
attributes: exception.record.errors.attribute_names
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
|
|
|
@ -53,6 +53,11 @@ class ContactAPI extends ApiClient {
|
|||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
filter(page = 1, sortAttr = 'name', queryPayload) {
|
||||
let requestURL = `${this.url}/filter?${buildContactParams(page, sortAttr)}`;
|
||||
return axios.post(requestURL, queryPayload);
|
||||
}
|
||||
|
||||
importContacts(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('import_file', file);
|
||||
|
|
|
@ -11,6 +11,7 @@ describe('#ContactsAPI', () => {
|
|||
expect(contactAPI).toHaveProperty('update');
|
||||
expect(contactAPI).toHaveProperty('delete');
|
||||
expect(contactAPI).toHaveProperty('getConversations');
|
||||
expect(contactAPI).toHaveProperty('filter');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
|
@ -81,6 +82,24 @@ describe('#ContactsAPI', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#filter', () => {
|
||||
const queryPayload = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'email',
|
||||
filter_operator: 'contains',
|
||||
values: ['fayaz'],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
contactAPI.filter(1, 'name', queryPayload);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/filter?include_contact_inboxes=false&page=1&sort=name',
|
||||
queryPayload
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ export default {
|
|||
currentUserID: 'getCurrentUserID',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
conversationStats: 'conversationStats/getStats',
|
||||
appliedFilters: 'getAppliedFilters',
|
||||
appliedFilters: 'getAppliedConversationFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
:key="attribute.key"
|
||||
:value="attribute.key"
|
||||
>
|
||||
{{ $t(`FILTER.ATTRIBUTES.${attribute.attributeI18nKey}`) }}
|
||||
{{ attribute.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
|
@ -73,7 +73,6 @@
|
|||
</div>
|
||||
<woot-button
|
||||
icon="dismiss"
|
||||
icon-size="16"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="removeFilter"
|
|
@ -49,9 +49,10 @@
|
|||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
import FilterInputBox from './components/FilterInput.vue';
|
||||
import FilterInputBox from '../FilterInput.vue';
|
||||
import languages from './advancedFilterItems/languages';
|
||||
import countries from './advancedFilterItems/countries';
|
||||
import countries from '/app/javascript/shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -94,19 +95,18 @@ export default {
|
|||
return this.filterTypes.map(type => {
|
||||
return {
|
||||
key: type.attributeKey,
|
||||
name: type.attributeName,
|
||||
attributeI18nKey: type.attributeI18nKey,
|
||||
name: this.$t(`FILTER.ATTRIBUTES.${type.attributeI18nKey}`),
|
||||
};
|
||||
});
|
||||
},
|
||||
getAppliedFilters() {
|
||||
return this.$store.getters.getAppliedFilters;
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedConversationFilters: 'getAppliedConversationFilters',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('campaigns/get');
|
||||
if (this.getAppliedFilters.length) {
|
||||
this.appliedFilters = this.getAppliedFilters;
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [...this.getAppliedConversationFilters];
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
|
@ -125,7 +125,6 @@ export default {
|
|||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.filterOperators;
|
||||
},
|
||||
// eslint-disable-next-line consistent-return
|
||||
getDropdownValues(type) {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
switch (type) {
|
||||
|
@ -169,7 +168,7 @@ export default {
|
|||
case 'country_code':
|
||||
return countries;
|
||||
default:
|
||||
break;
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
|
@ -190,11 +189,11 @@ export default {
|
|||
submitFilterQuery() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) return;
|
||||
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
|
||||
this.$store.dispatch(
|
||||
'setConversationFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
|
|
|
@ -103,13 +103,15 @@
|
|||
},
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "Enter the email address of the contact",
|
||||
"LABEL": "Email Address"
|
||||
"LABEL": "Email Address",
|
||||
"DUPLICATE": "This email address is in use for another contact."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Enter the phone number of the contact",
|
||||
"LABEL": "Phone Number",
|
||||
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]",
|
||||
"ERROR": "Phone number should be either empty or of E.164 format"
|
||||
"ERROR": "Phone number should be either empty or of E.164 format",
|
||||
"DUPLICATE": "This phone number is in use for another contact."
|
||||
},
|
||||
"LOCATION": {
|
||||
"PLACEHOLDER": "Enter the location of the contact",
|
||||
|
@ -139,7 +141,6 @@
|
|||
}
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact saved successfully",
|
||||
"CONTACT_ALREADY_EXIST": "This email address is in use for another contact.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"NEW_CONVERSATION": {
|
||||
|
@ -176,6 +177,7 @@
|
|||
"FIELDS": "Contact fields",
|
||||
"SEARCH_BUTTON": "Search",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
|
||||
"FILTER_CONTACTS": "Filter",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading contacts...",
|
||||
"404": "No contacts matches your search 🔍",
|
||||
|
|
34
app/javascript/dashboard/i18n/locale/en/contactFilters.json
Normal file
34
app/javascript/dashboard/i18n/locale/en/contactFilters.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"CONTACTS_FILTER": {
|
||||
"TITLE": "Filter Contacts",
|
||||
"SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.",
|
||||
"ADD_NEW_FILTER": "Add Filter",
|
||||
"CLEAR_ALL_FILTERS": "Clear All Filters",
|
||||
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
|
||||
"SUBMIT_BUTTON_LABEL": "Submit",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel",
|
||||
"CLEAR_BUTTON_LABEL": "Clear Filters",
|
||||
"EMPTY_VALUE_ERROR": "Value is required",
|
||||
"TOOLTIP_LABEL": "Filter contacts",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "AND",
|
||||
"OR": "OR"
|
||||
},
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
"contains": "Contains",
|
||||
"does_not_contain": "Does not contain",
|
||||
"is_present": "Is present",
|
||||
"is_not_present": "Is not present"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Name",
|
||||
"EMAIL": "Email",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"CITY": "City",
|
||||
"COUNTRY": "Country"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import { default as _signup } from './signup.json';
|
|||
import { default as _teamsSettings } from './teamsSettings.json';
|
||||
import { default as _advancedFilters } from './advancedFilters.json';
|
||||
import { default as _automation } from './automation.json';
|
||||
import { default as _contactFilters } from './contactFilters.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
|
@ -44,4 +45,5 @@ export default {
|
|||
..._teamsSettings,
|
||||
..._advancedFilters,
|
||||
..._automation,
|
||||
..._contactFilters,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<div class="column">
|
||||
<woot-modal-header :header-title="$t('CONTACTS_FILTER.TITLE')">
|
||||
<p>{{ $t('CONTACTS_FILTER.SUBTITLE') }}</p>
|
||||
</woot-modal-header>
|
||||
<div class="row modal-content">
|
||||
<div class="medium-12 columns filters-wrap">
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
:filter-attributes="filterAttributes"
|
||||
:input-type="getInputType(appliedFilters[i].attribute_key)"
|
||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:v="$v.appliedFilters.$each[i]"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
/>
|
||||
<div class="filter-actions">
|
||||
<woot-button
|
||||
icon="add"
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.ADD_NEW_FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters"
|
||||
icon="subtract"
|
||||
color-scheme="alert"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.CLEAR_ALL_FILTERS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-12 columns">
|
||||
<div class="modal-footer justify-content-end w-full">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CONTACTS_FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="submitFilterQuery">
|
||||
{{ $t('CONTACTS_FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import FilterInputBox from '../../../../components/widgets/FilterInput.vue';
|
||||
import countries from '/app/javascript/shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
filterTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
required,
|
||||
$each: {
|
||||
values: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterAttributes() {
|
||||
return this.filterTypes.map(type => {
|
||||
return {
|
||||
key: type.attributeKey,
|
||||
name: this.$t(`CONTACTS_FILTER.ATTRIBUTES.${type.attributeI18nKey}`),
|
||||
};
|
||||
});
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.getAppliedContactFilters.length) {
|
||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInputType(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type.filterOperators;
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
switch (type) {
|
||||
case 'country_code':
|
||||
return countries;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.appliedFilters.length <= 1) {
|
||||
this.showAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
||||
} else {
|
||||
this.appliedFilters.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitFilterQuery() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) return;
|
||||
this.$store.dispatch(
|
||||
'contacts/setContactFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.appliedFilters[this.appliedFilters.length - 1].query_operator = null;
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
).filterOperators[0].value;
|
||||
this.appliedFilters[index].values = '';
|
||||
},
|
||||
showUserInput(operatorType) {
|
||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
clearFilters() {
|
||||
this.$emit('clearFilters');
|
||||
this.onClose();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filters-wrap {
|
||||
padding: var(--space-normal);
|
||||
border-radius: var(--border-radius-large);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background-light);
|
||||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
</style>
|
|
@ -8,6 +8,7 @@
|
|||
:on-input-search="onInputSearch"
|
||||
:on-toggle-create="onToggleCreate"
|
||||
:on-toggle-import="onToggleImport"
|
||||
:on-toggle-filter="onToggleFilters"
|
||||
:header-title="label"
|
||||
/>
|
||||
<contacts-table
|
||||
|
@ -34,6 +35,19 @@
|
|||
<woot-modal :show.sync="showImportModal" :on-close="onToggleImport">
|
||||
<import-contacts v-if="showImportModal" :on-close="onToggleImport" />
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showFiltersModal"
|
||||
:on-close="onToggleFilters"
|
||||
size="medium"
|
||||
>
|
||||
<contacts-advanced-filters
|
||||
v-if="showFiltersModal"
|
||||
:on-close="onToggleFilters"
|
||||
:filter-types="contactFilterItems"
|
||||
@applyFilter="onApplyFilter"
|
||||
@clearFilters="clearFilters"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -46,6 +60,9 @@ 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';
|
||||
import ContactsAdvancedFilters from './ContactsAdvancedFilters.vue';
|
||||
import contactFilterItems from '../contactFilterItems';
|
||||
import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
|
||||
|
@ -57,6 +74,7 @@ export default {
|
|||
ContactInfoPanel,
|
||||
CreateContact,
|
||||
ImportContacts,
|
||||
ContactsAdvancedFilters,
|
||||
},
|
||||
props: {
|
||||
label: { type: String, default: '' },
|
||||
|
@ -68,6 +86,13 @@ export default {
|
|||
showImportModal: false,
|
||||
selectedContactId: '',
|
||||
sortConfig: { name: 'asc' },
|
||||
showFiltersModal: false,
|
||||
contactFilterItems: contactFilterItems.map(filter => ({
|
||||
...filter,
|
||||
attributeName: this.$t(
|
||||
`CONTACTS_FILTER.ATTRIBUTES.${filter.attributeI18nKey}`
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -188,6 +213,20 @@ export default {
|
|||
this.sortConfig = params;
|
||||
this.fetchContacts(this.meta.currentPage);
|
||||
},
|
||||
onToggleFilters() {
|
||||
this.showFiltersModal = !this.showFiltersModal;
|
||||
},
|
||||
onApplyFilter(payload) {
|
||||
this.closeContactInfoPanel();
|
||||
this.$store.dispatch('contacts/filter', {
|
||||
queryPayload: filterQueryGenerator(payload),
|
||||
});
|
||||
this.showFiltersModal = false;
|
||||
},
|
||||
clearFilters() {
|
||||
this.$store.dispatch('contacts/clearContactFilters');
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -19,17 +19,29 @@
|
|||
/>
|
||||
<woot-button
|
||||
:is-loading="false"
|
||||
class="clear"
|
||||
:class-names="searchButtonClass"
|
||||
@click="onSearchSubmit"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<div class="filters__button-wrap">
|
||||
<div v-if="hasAppliedFilters" class="filters__applied-indicator" />
|
||||
<woot-button
|
||||
class="margin-right-small clear"
|
||||
color-scheme="secondary"
|
||||
data-testid="create-new-contact"
|
||||
icon="filter"
|
||||
@click="onToggleFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<woot-button
|
||||
class="margin-right-small clear"
|
||||
color-scheme="success"
|
||||
icon="add-circle"
|
||||
class="margin-right-small"
|
||||
icon="person-add"
|
||||
data-testid="create-new-contact"
|
||||
@click="onToggleCreate"
|
||||
>
|
||||
|
@ -38,7 +50,8 @@
|
|||
|
||||
<woot-button
|
||||
color-scheme="info"
|
||||
icon="cloud-backup"
|
||||
icon="upload"
|
||||
class="clear"
|
||||
@click="onToggleImport"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
|
@ -49,6 +62,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: {
|
||||
|
@ -75,6 +90,10 @@ export default {
|
|||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onToggleFilter: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -86,6 +105,12 @@ export default {
|
|||
searchButtonClass() {
|
||||
return this.searchQuery !== '' ? 'show' : '';
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -155,4 +180,17 @@ export default {
|
|||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.filters__button-wrap {
|
||||
position: relative;
|
||||
|
||||
.filters__applied-indicator {
|
||||
position: absolute;
|
||||
height: var(--space-small);
|
||||
width: var(--space-small);
|
||||
top: var(--space-smaller);
|
||||
right: var(--space-slab);
|
||||
background-color: var(--s-500);
|
||||
border-radius: var(--border-radius-rounded);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
const filterTypes = [
|
||||
{
|
||||
attributeKey: 'name',
|
||||
attributeI18nKey: 'NAME',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'email',
|
||||
attributeI18nKey: 'EMAIL',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'phone_number',
|
||||
attributeI18nKey: 'PHONE_NUMBER',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'identifier',
|
||||
attributeI18nKey: 'IDENTIFIER',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'country_code',
|
||||
attributeI18nKey: 'COUNTRY',
|
||||
inputType: 'search_select',
|
||||
dataType: 'number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
{
|
||||
attributeKey: 'city',
|
||||
attributeI18nKey: 'CITY',
|
||||
inputType: 'plain_text',
|
||||
dataType: 'Number',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'contains',
|
||||
label: 'Contains',
|
||||
},
|
||||
{
|
||||
value: 'does_not_contain',
|
||||
label: 'Does not contain',
|
||||
},
|
||||
],
|
||||
attribute_type: 'standard',
|
||||
},
|
||||
];
|
||||
|
||||
export default filterTypes;
|
|
@ -121,8 +121,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
hasADuplicateContact: false,
|
||||
duplicateContact: {},
|
||||
companyName: '',
|
||||
description: '',
|
||||
email: '',
|
||||
|
@ -201,12 +199,7 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
resetDuplicate() {
|
||||
this.hasADuplicateContact = false;
|
||||
this.duplicateContact = {};
|
||||
},
|
||||
async handleSubmit() {
|
||||
this.resetDuplicate();
|
||||
this.$v.$touch();
|
||||
|
||||
if (this.$v.$invalid) {
|
||||
|
@ -218,9 +211,13 @@ export default {
|
|||
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'));
|
||||
if (error.data.includes('email')) {
|
||||
this.showAlert(
|
||||
this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE')
|
||||
);
|
||||
} else if (error.data.includes('phone_number')) {
|
||||
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
this.showAlert(error.data);
|
||||
} else {
|
||||
|
|
|
@ -60,8 +60,8 @@ export const actions = {
|
|||
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||
if (error.response?.data?.contact) {
|
||||
throw new DuplicateContactException(error.response.data.contact);
|
||||
if (error.response?.status === 422) {
|
||||
throw new DuplicateContactException(error.response.data.attributes);
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
@ -179,4 +179,27 @@ export const actions = {
|
|||
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
},
|
||||
|
||||
filter: async ({ commit }, { page = 1, sortAttr, queryPayload } = {}) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await ContactAPI.filter(page, sortAttr, queryPayload);
|
||||
commit(types.CLEAR_CONTACTS);
|
||||
commit(types.SET_CONTACTS, payload);
|
||||
commit(types.SET_CONTACT_META, meta);
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
setContactFilters({ commit }, data) {
|
||||
commit(types.SET_CONTACT_FILTERS, data);
|
||||
},
|
||||
|
||||
clearContactFilters({ commit }) {
|
||||
commit(types.CLEAR_CONTACT_FILTERS);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,4 +12,7 @@ export const getters = {
|
|||
getMeta: $state => {
|
||||
return $state.meta;
|
||||
},
|
||||
getAppliedContactFilters: _state => {
|
||||
return _state.appliedFilters;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ const state = {
|
|||
isDeleting: false,
|
||||
},
|
||||
sortOrder: [],
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -66,4 +66,12 @@ export const mutations = {
|
|||
}
|
||||
});
|
||||
},
|
||||
|
||||
[types.SET_CONTACT_FILTERS](_state, data) {
|
||||
_state.appliedFilters = data;
|
||||
},
|
||||
|
||||
[types.CLEAR_CONTACT_FILTERS](_state) {
|
||||
_state.appliedFilters = [];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -31,7 +31,7 @@ const getters = {
|
|||
return isChatMine;
|
||||
});
|
||||
},
|
||||
getAppliedFilters: _state => {
|
||||
getAppliedConversationFilters: _state => {
|
||||
return _state.appliedFilters;
|
||||
},
|
||||
getUnAssignedChats: _state => activeFilters => {
|
||||
|
|
|
@ -6,9 +6,21 @@ import {
|
|||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from '../../../../../shared/helpers/CustomErrors';
|
||||
import { filterApiResponse } from './filterApiResponse';
|
||||
|
||||
const { actions } = Contacts;
|
||||
|
||||
const filterQueryData = {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'email',
|
||||
filter_operator: 'contains',
|
||||
values: ['fayaz'],
|
||||
query_operator: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
@ -82,9 +94,10 @@ describe('#actions', () => {
|
|||
it('sends correct actions if duplicate contact is found', async () => {
|
||||
axios.patch.mockRejectedValue({
|
||||
response: {
|
||||
status: 422,
|
||||
data: {
|
||||
message: 'Incorrect header',
|
||||
contact: { id: 1, name: 'contact-name' },
|
||||
attributes: ['email'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -247,4 +260,43 @@ describe('#actions', () => {
|
|||
).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchFilteredContacts', () => {
|
||||
it('fetches filtered conversations with a mock commit', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
data: filterApiResponse,
|
||||
});
|
||||
await actions.filter({ commit }, filterQueryData);
|
||||
expect(commit).toHaveBeenCalledTimes(5);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['SET_CONTACT_UI_FLAG', { isFetching: true }],
|
||||
['CLEAR_CONTACTS'],
|
||||
['SET_CONTACTS', filterApiResponse.payload],
|
||||
['SET_CONTACT_META', filterApiResponse.meta],
|
||||
['SET_CONTACT_UI_FLAG', { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setContactsFilter', () => {
|
||||
it('commits the correct mutation and sets filter state', () => {
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'email',
|
||||
filter_operator: 'contains',
|
||||
values: ['fayaz'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
actions.setContactFilters({ commit }, filters);
|
||||
expect(commit.mock.calls).toEqual([[types.SET_CONTACT_FILTERS, filters]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearContactFilters', () => {
|
||||
it('commits the correct mutation and clears filter state', () => {
|
||||
actions.clearContactFilters({ commit });
|
||||
expect(commit.mock.calls).toEqual([[types.CLEAR_CONTACT_FILTERS]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
export const filterApiResponse = {
|
||||
meta: {
|
||||
count: {
|
||||
all_count: 2,
|
||||
},
|
||||
current_page: '1',
|
||||
},
|
||||
payload: [
|
||||
{
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: 'fayaz@g.com',
|
||||
id: 8,
|
||||
name: 'fayaz',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/f2e86d3a78353cdf51002f44cf6ea846?d=404',
|
||||
custom_attributes: {},
|
||||
conversations_count: 1,
|
||||
last_activity_at: 1631081845,
|
||||
},
|
||||
{
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: 'fayaz@gma.com',
|
||||
id: 9,
|
||||
name: 'fayaz',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/792af86e3ad4591552e1025a6415baa6?d=404',
|
||||
custom_attributes: {},
|
||||
conversations_count: 1,
|
||||
last_activity_at: 1631614585,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -36,4 +36,18 @@ describe('#getters', () => {
|
|||
isUpdating: false,
|
||||
});
|
||||
});
|
||||
it('getAppliedContactFilters', () => {
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'email',
|
||||
filter_operator: 'contains',
|
||||
values: 'a',
|
||||
query_operator: null,
|
||||
},
|
||||
];
|
||||
const state = {
|
||||
appliedFilters: filters,
|
||||
};
|
||||
expect(getters.getAppliedContactFilters(state)).toEqual(filters);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,4 +64,42 @@ describe('#mutations', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CONTACT_FILTERS', () => {
|
||||
it('set contact filter', () => {
|
||||
const appliedFilters = [
|
||||
{
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['fayaz'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
mutations[types.SET_CONTACT_FILTERS](appliedFilters);
|
||||
expect(appliedFilters).toEqual([
|
||||
{
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['fayaz'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#CLEAR_CONTACT_FILTERS', () => {
|
||||
it('clears applied contact filters', () => {
|
||||
const state = {
|
||||
appliedFilters: [
|
||||
{
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['fayaz'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
};
|
||||
mutations[types.CLEAR_CONTACT_FILTERS](state);
|
||||
expect(state.appliedFilters).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,8 +115,8 @@ describe('#getters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getAppliedFilters', () => {
|
||||
it('getAppliedFilters', () => {
|
||||
describe('#getAppliedConversationFilters', () => {
|
||||
it('getAppliedConversationFilters', () => {
|
||||
const filtersList = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
|
@ -128,7 +128,7 @@ describe('#getters', () => {
|
|||
const state = {
|
||||
appliedFilters: filtersList,
|
||||
};
|
||||
expect(getters.getAppliedFilters(state)).toEqual(filtersList);
|
||||
expect(getters.getAppliedConversationFilters(state)).toEqual(filtersList);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -108,6 +108,8 @@ export default {
|
|||
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||
DELETE_CONTACT: 'DELETE_CONTACT',
|
||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||
SET_CONTACT_FILTERS: 'SET_CONTACT_FILTERS',
|
||||
CLEAR_CONTACT_FILTERS: 'CLEAR_CONTACT_FILTERS',
|
||||
|
||||
// Notifications
|
||||
SET_NOTIFICATIONS_META: 'SET_NOTIFICATIONS_META',
|
||||
|
|
|
@ -4,4 +4,5 @@
|
|||
--border-radius-normal: 0.5rem;
|
||||
--border-radius-medium: 0.7rem;
|
||||
--border-radius-large: 0.9rem;
|
||||
--border-radius-rounded: 50%;
|
||||
}
|
||||
|
|
|
@ -73,17 +73,19 @@
|
|||
"tag-outline": "M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465Zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z",
|
||||
"video-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z",
|
||||
"warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z",
|
||||
|
||||
|
||||
"brand-whatsapp-outline": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z",
|
||||
"brand-twitter-outline": "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z",
|
||||
"brand-facebook-outline": "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z",
|
||||
"brand-line-outline": "M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314",
|
||||
"brand-sms-outline": "M272.1 204.2v71.1c0 1.8-1.4 3.2-3.2 3.2h-11.4c-1.1 0-2.1-.6-2.6-1.3l-32.6-44v42.2c0 1.8-1.4 3.2-3.2 3.2h-11.4c-1.8 0-3.2-1.4-3.2-3.2v-71.1c0-1.8 1.4-3.2 3.2-3.2H219c1 0 2.1 .5 2.6 1.4l32.6 44v-42.2c0-1.8 1.4-3.2 3.2-3.2h11.4c1.8-.1 3.3 1.4 3.3 3.1zm-82-3.2h-11.4c-1.8 0-3.2 1.4-3.2 3.2v71.1c0 1.8 1.4 3.2 3.2 3.2h11.4c1.8 0 3.2-1.4 3.2-3.2v-71.1c0-1.7-1.4-3.2-3.2-3.2zm-27.5 59.6h-31.1v-56.4c0-1.8-1.4-3.2-3.2-3.2h-11.4c-1.8 0-3.2 1.4-3.2 3.2v71.1c0 .9 .3 1.6 .9 2.2 .6 .5 1.3 .9 2.2 .9h45.7c1.8 0 3.2-1.4 3.2-3.2v-11.4c0-1.7-1.4-3.2-3.1-3.2zM332.1 201h-45.7c-1.7 0-3.2 1.4-3.2 3.2v71.1c0 1.7 1.4 3.2 3.2 3.2h45.7c1.8 0 3.2-1.4 3.2-3.2v-11.4c0-1.8-1.4-3.2-3.2-3.2H301v-12h31.1c1.8 0 3.2-1.4 3.2-3.2V234c0-1.8-1.4-3.2-3.2-3.2H301v-12h31.1c1.8 0 3.2-1.4 3.2-3.2v-11.4c-.1-1.7-1.5-3.2-3.2-3.2zM448 113.7V399c-.1 44.8-36.8 81.1-81.7 81H81c-44.8-.1-81.1-36.9-81-81.7V113c.1-44.8 36.9-81.1 81.7-81H367c44.8 .1 81.1 36.8 81 81.7zm-61.6 122.6c0-73-73.2-132.4-163.1-132.4-89.9 0-163.1 59.4-163.1 132.4 0 65.4 58 120.2 136.4 130.6 19.1 4.1 16.9 11.1 12.6 36.8-.7 4.1-3.3 16.1 14.1 8.8 17.4-7.3 93.9-55.3 128.2-94.7 23.6-26 34.9-52.3 34.9-81.5z",
|
||||
"brand-telegram-outline": "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
|
||||
"brand-sms-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.644-1.142l-4.29 1.117a.85.85 0 0 1-1.037-1.036l1.116-4.289A9.959 9.959 0 0 1 2 12C2 6.477 6.477 2 12 2Zm1.252 11H8.75l-.102.007a.75.75 0 0 0 0 1.486l.102.007h4.502l.101-.007a.75.75 0 0 0 0-1.486L13.252 13Zm1.998-3.5h-6.5l-.102.007a.75.75 0 0 0 0 1.486L8.75 11h6.5l.102-.007a.75.75 0 0 0 0-1.486L15.25 9.5Z",
|
||||
"brand-telegram-outline":"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
|
||||
"brand-linkedin-outline": "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",
|
||||
|
||||
"add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z",
|
||||
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z",
|
||||
"checkmark-circle-solid": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z"
|
||||
"subtract-outline": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z",
|
||||
"checkmark-circle-solid": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z",
|
||||
"person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z",
|
||||
"upload-outline": "M6.087 7.75a5.752 5.752 0 0 1 11.326 0h.087a4 4 0 0 1 3.962 4.552 6.534 6.534 0 0 0-1.597-1.364A2.501 2.501 0 0 0 17.5 9.25h-.756a.75.75 0 0 1-.75-.713 4.25 4.25 0 0 0-8.489 0 .75.75 0 0 1-.749.713H6a2.5 2.5 0 0 0 0 5h4.4a6.458 6.458 0 0 0-.357 1.5H6a4 4 0 0 1 0-8h.087ZM22 16.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0Zm-6-1.793V19.5a.5.5 0 0 0 1 0v-4.793l1.646 1.647a.5.5 0 0 0 .708-.708l-2.5-2.5a.5.5 0 0 0-.708 0l-2.5 2.5a.5.5 0 0 0 .708.708L16 14.707Z"
|
||||
}
|
||||
|
|
|
@ -3,15 +3,11 @@ const { DuplicateContactException } = require('../CustomErrors');
|
|||
describe('DuplicateContactException', () => {
|
||||
it('returns correct exception', () => {
|
||||
const exception = new DuplicateContactException({
|
||||
id: 1,
|
||||
name: 'contact-name',
|
||||
email: 'email@example.com',
|
||||
attributes: ['email'],
|
||||
});
|
||||
expect(exception.message).toEqual('DUPLICATE_CONTACT');
|
||||
expect(exception.data).toEqual({
|
||||
id: 1,
|
||||
name: 'contact-name',
|
||||
email: 'email@example.com',
|
||||
attributes: ['email'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,9 +4,7 @@ class Contacts::FilterService < FilterService
|
|||
|
||||
{
|
||||
contacts: @contacts,
|
||||
count: {
|
||||
all_count: @contacts.count
|
||||
}
|
||||
count: @contacts.count
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -28,21 +26,36 @@ class Contacts::FilterService < FilterService
|
|||
|
||||
case current_filter['attribute_type']
|
||||
when 'additional_attributes'
|
||||
" contacts.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||
" LOWER(contacts.additional_attributes ->> '#{attribute_key}') #{filter_operator_value} #{query_operator} "
|
||||
when 'standard'
|
||||
if attribute_key == 'labels'
|
||||
" tags.id #{filter_operator_value} #{query_operator} "
|
||||
else
|
||||
" contacts.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||
" LOWER(contacts.#{attribute_key}) #{filter_operator_value} #{query_operator} "
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def filter_values(query_hash)
|
||||
query_hash['values'][0]
|
||||
current_val = query_hash['values'][0]
|
||||
if query_hash['attribute_key'] == 'phone_number'
|
||||
"+#{current_val}"
|
||||
elsif query_hash['attribute_key'] == 'country_code'
|
||||
current_val.downcase
|
||||
else
|
||||
current_val.is_a?(String) ? current_val.downcase : current_val
|
||||
end
|
||||
end
|
||||
|
||||
def base_relation
|
||||
Current.account.contacts.left_outer_joins(:labels)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def equals_to_filter_string(filter_operator, current_index)
|
||||
return "= :value_#{current_index}" if filter_operator == 'equal_to'
|
||||
|
||||
"!= :value_#{current_index}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
json.array! @contacts do |contact|
|
||||
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact
|
||||
json.meta do
|
||||
json.count @contacts_count
|
||||
json.current_page @current_page
|
||||
end
|
||||
|
||||
json.payload do
|
||||
json.array! @contacts do |contact|
|
||||
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact, with_contact_inboxes: @include_contact_inboxes
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
"filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ],
|
||||
"attribute_type": "standard"
|
||||
},
|
||||
"contact_identifier": {
|
||||
"identifier": {
|
||||
"attribute_name": "Contact Identifier",
|
||||
"input_type": "search_box with name tags/plain text",
|
||||
"data_type": "text",
|
||||
|
|
|
@ -448,7 +448,19 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['contact']['id']).to eq(other_contact.id)
|
||||
expect(JSON.parse(response.body)['attributes']).to include('email')
|
||||
end
|
||||
|
||||
it 'prevents updating with an existing phone number' do
|
||||
other_contact = create(:contact, account: account, phone_number: '+12000000')
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_params[:contact].merge({ phone_number: other_contact.phone_number }),
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(JSON.parse(response.body)['attributes']).to include('phone_number')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue