From d7cfe6858e4368a9c96438f19a3c2db745a43a90 Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:42:44 +0530 Subject: [PATCH 1/2] feat: Add advanced contact filters (#3471) Co-authored-by: Tejaswini Co-authored-by: Pranav Raj S Co-authored-by: Muhsin Keloth --- .codeclimate.yml | 3 +- app/javascript/dashboard/api/contacts.js | 5 + .../dashboard/api/specs/contacts.spec.js | 19 ++ .../dashboard/components/ChatList.vue | 2 +- .../components => }/FilterInput.vue | 3 +- .../ConversationAdvancedFilter.vue | 23 +- .../dashboard/i18n/locale/en/contact.json | 1 + .../i18n/locale/en/contactFilters.json | 34 +++ .../dashboard/i18n/locale/en/index.js | 2 + .../components/ContactsAdvancedFilters.vue | 197 ++++++++++++++++++ .../contacts/components/ContactsView.vue | 41 +++- .../dashboard/contacts/components/Header.vue | 46 +++- .../contacts/contactFilterItems/index.js | 138 ++++++++++++ .../store/modules/contacts/actions.js | 23 ++ .../store/modules/contacts/getters.js | 3 + .../dashboard/store/modules/contacts/index.js | 1 + .../store/modules/contacts/mutations.js | 8 + .../store/modules/conversations/getters.js | 2 +- .../modules/specs/contacts/actions.spec.js | 53 ++++- .../specs/contacts/filterApiResponse.js | 38 ++++ .../modules/specs/contacts/getters.spec.js | 14 ++ .../modules/specs/contacts/mutations.spec.js | 38 ++++ .../specs/conversations/getters.spec.js | 6 +- .../dashboard/store/mutation-types.js | 2 + .../assets/stylesheets/border-radius.scss | 1 + .../FluentIcon/dashboard-icons.json | 18 +- .../constants}/countries.js | 0 app/services/contacts/filter_service.rb | 25 ++- .../v1/accounts/contacts/filter.json.jbuilder | 11 +- lib/filters/filter_keys.json | 2 +- 30 files changed, 716 insertions(+), 43 deletions(-) rename app/javascript/dashboard/components/widgets/{conversation/components => }/FilterInput.vue (98%) create mode 100644 app/javascript/dashboard/i18n/locale/en/contactFilters.json create mode 100644 app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue create mode 100644 app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js create mode 100644 app/javascript/dashboard/store/modules/specs/contacts/filterApiResponse.js rename app/javascript/{dashboard/components/widgets/conversation/advancedFilterItems => shared/constants}/countries.js (100%) diff --git a/.codeclimate.yml b/.codeclimate.yml index 9e8b209bd..af0c0714f 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -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" diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 08c969248..8bea5f74b 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -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); diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js index 08e6720d7..dfdc3a48b 100644 --- a/app/javascript/dashboard/api/specs/contacts.spec.js +++ b/app/javascript/dashboard/api/specs/contacts.spec.js @@ -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 + ); + }); }); }); diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index b5053a292..1517105df 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -156,7 +156,7 @@ export default { currentUserID: 'getCurrentUserID', activeInbox: 'getSelectedInbox', conversationStats: 'conversationStats/getStats', - appliedFilters: 'getAppliedFilters', + appliedFilters: 'getAppliedConversationFilters', }), hasAppliedFilters() { return this.appliedFilters.length; diff --git a/app/javascript/dashboard/components/widgets/conversation/components/FilterInput.vue b/app/javascript/dashboard/components/widgets/FilterInput.vue similarity index 98% rename from app/javascript/dashboard/components/widgets/conversation/components/FilterInput.vue rename to app/javascript/dashboard/components/widgets/FilterInput.vue index 45592bed1..e0d0ea3b6 100644 --- a/app/javascript/dashboard/components/widgets/conversation/components/FilterInput.vue +++ b/app/javascript/dashboard/components/widgets/FilterInput.vue @@ -12,7 +12,7 @@ :key="attribute.key" :value="attribute.key" > - {{ $t(`FILTER.ATTRIBUTES.${attribute.attributeI18nKey}`) }} + {{ attribute.name }} @@ -73,7 +73,6 @@ 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) { diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index a06331b4b..018a2acc1 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -176,6 +176,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 🔍", diff --git a/app/javascript/dashboard/i18n/locale/en/contactFilters.json b/app/javascript/dashboard/i18n/locale/en/contactFilters.json new file mode 100644 index 000000000..8ebe460bb --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/contactFilters.json @@ -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" + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 9555b40b3..c769ab472 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -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, }; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue new file mode 100644 index 000000000..5c0aae26c --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index 6b1d6f6ed..1c7489e71 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -8,6 +8,7 @@ :on-input-search="onInputSearch" :on-toggle-create="onToggleCreate" :on-toggle-import="onToggleImport" + :on-toggle-filter="onToggleFilters" :header-title="label" /> + + + @@ -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: { @@ -133,7 +158,7 @@ export default { fetchContacts(page) { this.updatePageParam(page); let value = ''; - if(this.searchQuery.charAt(0) === '+') { + if (this.searchQuery.charAt(0) === '+') { value = this.searchQuery.substring(1); } else { value = this.searchQuery; @@ -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); + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue index 87d2e5177..2758da76d 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/Header.vue @@ -19,17 +19,29 @@ /> {{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }} - +
+
+ + {{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }} + +
@@ -38,7 +50,8 @@ {{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }} @@ -49,6 +62,8 @@ @@ -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); + } +} diff --git a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js new file mode 100644 index 000000000..fae8ac456 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js @@ -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; diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js index 1cea2aad2..44bef1144 100644 --- a/app/javascript/dashboard/store/modules/contacts/actions.js +++ b/app/javascript/dashboard/store/modules/contacts/actions.js @@ -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); + }, }; diff --git a/app/javascript/dashboard/store/modules/contacts/getters.js b/app/javascript/dashboard/store/modules/contacts/getters.js index cce21f796..c72176171 100644 --- a/app/javascript/dashboard/store/modules/contacts/getters.js +++ b/app/javascript/dashboard/store/modules/contacts/getters.js @@ -12,4 +12,7 @@ export const getters = { getMeta: $state => { return $state.meta; }, + getAppliedContactFilters: _state => { + return _state.appliedFilters; + }, }; diff --git a/app/javascript/dashboard/store/modules/contacts/index.js b/app/javascript/dashboard/store/modules/contacts/index.js index b2657ca78..ba8d47642 100644 --- a/app/javascript/dashboard/store/modules/contacts/index.js +++ b/app/javascript/dashboard/store/modules/contacts/index.js @@ -17,6 +17,7 @@ const state = { isDeleting: false, }, sortOrder: [], + appliedFilters: [], }; export default { diff --git a/app/javascript/dashboard/store/modules/contacts/mutations.js b/app/javascript/dashboard/store/modules/contacts/mutations.js index 9e8e64e6d..51cf788bf 100644 --- a/app/javascript/dashboard/store/modules/contacts/mutations.js +++ b/app/javascript/dashboard/store/modules/contacts/mutations.js @@ -66,4 +66,12 @@ export const mutations = { } }); }, + + [types.SET_CONTACT_FILTERS](_state, data) { + _state.appliedFilters = data; + }, + + [types.CLEAR_CONTACT_FILTERS](_state) { + _state.appliedFilters = []; + }, }; diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index 66682f2b8..e4b063899 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -31,7 +31,7 @@ const getters = { return isChatMine; }); }, - getAppliedFilters: _state => { + getAppliedConversationFilters: _state => { return _state.appliedFilters; }, getUnAssignedChats: _state => activeFilters => { diff --git a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js index 486b3e203..08b1b2067 100644 --- a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js @@ -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'); @@ -228,7 +240,7 @@ describe('#actions', () => { ]); }); }); - describe('#deleteCustomAttributes', () => { + describe('#deleteCustomAttributes', () => { it('sends correct mutations if API is success', async () => { axios.post.mockResolvedValue({ data: { payload: contactList[0] } }); await actions.deleteCustomAttributes( @@ -247,4 +259,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]]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/contacts/filterApiResponse.js b/app/javascript/dashboard/store/modules/specs/contacts/filterApiResponse.js new file mode 100644 index 000000000..df9656aff --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/contacts/filterApiResponse.js @@ -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, + }, + ], +}; diff --git a/app/javascript/dashboard/store/modules/specs/contacts/getters.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/getters.spec.js index 62c33ee51..38973ec9d 100644 --- a/app/javascript/dashboard/store/modules/specs/contacts/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contacts/getters.spec.js @@ -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); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/contacts/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/mutations.spec.js index f725c88ba..4eb40ff61 100644 --- a/app/javascript/dashboard/store/modules/specs/contacts/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contacts/mutations.spec.js @@ -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([]); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index 2d5936c17..bb718bb24 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -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); }); }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index e5e70f717..b723776c5 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -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', diff --git a/app/javascript/shared/assets/stylesheets/border-radius.scss b/app/javascript/shared/assets/stylesheets/border-radius.scss index 9e228fdf2..69a2a804a 100644 --- a/app/javascript/shared/assets/stylesheets/border-radius.scss +++ b/app/javascript/shared/assets/stylesheets/border-radius.scss @@ -4,4 +4,5 @@ --border-radius-normal: 0.5rem; --border-radius-medium: 0.7rem; --border-radius-large: 0.9rem; + --border-radius-rounded: 50%; } diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index b0bd49cdc..5e73456fd 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -8,7 +8,7 @@ "arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z", "arrow-swap-outline": "m14.783 2.22 4.495 4.494a.75.75 0 0 1 .073.976l-.072.085-4.495 4.504a.75.75 0 0 1-1.135-.975l.073-.084 3.217-3.223H5.243A.75.75 0 0 1 4.5 7.35l-.007-.101a.75.75 0 0 1 .648-.743l.102-.007 11.697-.001-3.218-3.217a.75.75 0 0 1-.072-.976l.072-.084a.75.75 0 0 1 .977-.073l.084.073 4.495 4.494-4.495-4.494ZM19.5 16.65l.006.1a.75.75 0 0 1-.648.744l-.102.007L7.063 17.5l3.22 3.22a.75.75 0 0 1 .074.976l-.073.084a.75.75 0 0 1-.976.073l-.085-.072-4.5-4.497a.75.75 0 0 1-.073-.976l.073-.084 4.5-4.504a.75.75 0 0 1 1.134.976l-.073.084L7.066 16h11.692a.75.75 0 0 1 .743.65l.006.1-.006-.1Z", "arrow-trending-lines-outline": "M16.749 2h4.554l.1.014.099.028.06.026c.08.034.153.085.219.15l.04.044.044.057.054.09.039.09.019.064.014.064.009.095v4.532a.75.75 0 0 1-1.493.102l-.007-.102V4.559l-6.44 6.44a.75.75 0 0 1-.976.073L13 11 9.97 8.09l-5.69 5.689a.75.75 0 0 1-1.133-.977l.073-.084 6.22-6.22a.75.75 0 0 1 .976-.072l.084.072 3.03 2.91L19.438 3.5h-2.69a.75.75 0 0 1-.742-.648l-.007-.102a.75.75 0 0 1 .648-.743L16.75 2ZM3.75 17a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5a.75.75 0 0 1 .75-.75Zm5.75-3.25a.75.75 0 0 0-1.5 0v7.5a.75.75 0 0 0 1.5 0v-7.5ZM13.75 15a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5a.75.75 0 0 1 .75-.75Zm5.75-4.25a.75.75 0 0 0-1.5 0v10.5a.75.75 0 0 0 1.5 0v-10.5Z", - "arrow-up-outline":"M4.21 10.733a.75.75 0 0 0 1.086 1.034l5.954-6.251V20.25a.75.75 0 0 0 1.5 0V5.516l5.955 6.251a.75.75 0 0 0 1.086-1.034l-7.067-7.42a.995.995 0 0 0-.58-.3.754.754 0 0 0-.29.001.995.995 0 0 0-.578.3L4.21 10.733Z", + "arrow-up-outline": "M4.21 10.733a.75.75 0 0 0 1.086 1.034l5.954-6.251V20.25a.75.75 0 0 0 1.5 0V5.516l5.955 6.251a.75.75 0 0 0 1.086-1.034l-7.067-7.42a.995.995 0 0 0-.58-.3.754.754 0 0 0-.29.001.995.995 0 0 0-.578.3L4.21 10.733Z", "attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z", "autocorrect-outline": "M13.461 4.934c.293.184.548.42.752.698l.117.171 2.945 4.696H21.5a.75.75 0 0 1 .743.649l.007.102a.75.75 0 0 1-.75.75l-3.284-.001.006.009-.009-.01a4.75 4.75 0 1 1-3.463-1.5h.756L13.059 6.6a1.25 1.25 0 0 0-2.04-.112l-.078.112-7.556 12.048a.75.75 0 0 1-1.322-.699l.052-.098L9.67 5.803a2.75 2.75 0 0 1 3.791-.869ZM14.751 12a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Z", "book-contacts-outline": "M15.5 12.25a.75.75 0 0 0-.75-.75h-5a.75.75 0 0 0-.75.75v.5c0 1 1.383 1.75 3.25 1.75s3.25-.75 3.25-1.75v-.5ZM14 8.745C14 7.78 13.217 7 12.25 7s-1.75.779-1.75 1.745a1.75 1.75 0 1 0 3.5 0ZM4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15Zm1.5 0V18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1Z", @@ -48,9 +48,9 @@ "location-outline": "M5.843 4.568a8.707 8.707 0 1 1 12.314 12.314l-1.187 1.174c-.875.858-2.01 1.962-3.406 3.312a2.25 2.25 0 0 1-3.128 0l-3.491-3.396c-.439-.431-.806-.794-1.102-1.09a8.707 8.707 0 0 1 0-12.314Zm11.253 1.06A7.207 7.207 0 1 0 6.904 15.822L8.39 17.29a753.98 753.98 0 0 0 3.088 3 .75.75 0 0 0 1.043 0l3.394-3.3c.47-.461.863-.85 1.18-1.168a7.207 7.207 0 0 0 0-10.192ZM12 7.999a3.002 3.002 0 1 1 0 6.004 3.002 3.002 0 0 1 0-6.003Zm0 1.5a1.501 1.501 0 1 0 0 3.004 1.501 1.501 0 0 0 0-3.003Z", "lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z", "mail-inbox-all-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3Zm2.075 11.5H4.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188Zm9.425-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Zm-11 5h10.5a.75.75 0 0 1 .102 1.493L17.25 11H6.75a.75.75 0 0 1-.102-1.493L6.75 9.5h10.5-10.5Zm0-3h10.5a.75.75 0 0 1 .102 1.493L17.25 8H6.75a.75.75 0 0 1-.102-1.493L6.75 6.5h10.5-10.5Z", - "mail-outline":"M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z", + "mail-outline": "M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z", "map-outline": "m9.203 4 .047-.002.046.001a.73.73 0 0 1 .067.007l.016.004c.086.014.17.044.252.092l.051.034 5.07 3.565L19.82 4.14a.75.75 0 0 1 1.174.51l.007.104v10.632a.75.75 0 0 1-.238.548l-.08.066-5.5 3.866a.744.744 0 0 1-.828.023L9.25 16.297l-5.07 3.565a.75.75 0 0 1-1.174-.51l-.007-.104V8.616a.75.75 0 0 1 .238-.548l.08-.066 5.5-3.866a.762.762 0 0 1 .2-.101l.122-.028.064-.008Zm10.298 2.197-4 2.812v8.799l4-2.812v-8.8ZM8.5 6.193l-4 2.812v8.8l4-2.813V6.193Zm1.502 0v8.8l4 2.811V9.005l-4-2.812Z", - "megaphone-outline":"M21.907 5.622c.062.208.093.424.093.641V17.74a2.25 2.25 0 0 1-2.891 2.156l-5.514-1.64a4.002 4.002 0 0 1-7.59-1.556L6 16.5l-.001-.5-2.39-.711A2.25 2.25 0 0 1 2 13.131V10.87a2.25 2.25 0 0 1 1.61-2.156l15.5-4.606a2.25 2.25 0 0 1 2.797 1.515ZM7.499 16.445l.001.054a2.5 2.5 0 0 0 4.624 1.321l-4.625-1.375Zm12.037-10.9-15.5 4.605a.75.75 0 0 0-.536.72v2.261a.75.75 0 0 0 .536.72l15.5 4.607a.75.75 0 0 0 .964-.72V6.264a.75.75 0 0 0-.964-.719Z", + "megaphone-outline": "M21.907 5.622c.062.208.093.424.093.641V17.74a2.25 2.25 0 0 1-2.891 2.156l-5.514-1.64a4.002 4.002 0 0 1-7.59-1.556L6 16.5l-.001-.5-2.39-.711A2.25 2.25 0 0 1 2 13.131V10.87a2.25 2.25 0 0 1 1.61-2.156l15.5-4.606a2.25 2.25 0 0 1 2.797 1.515ZM7.499 16.445l.001.054a2.5 2.5 0 0 0 4.624 1.321l-4.625-1.375Zm12.037-10.9-15.5 4.605a.75.75 0 0 0-.536.72v2.261a.75.75 0 0 0 .536.72l15.5 4.607a.75.75 0 0 0 .964-.72V6.264a.75.75 0 0 0-.964-.719Z", "merge-outline": "M3 6.75A.75.75 0 0 1 3.75 6h4.5a.75.75 0 0 1 .53.22L13.56 11h5.878L15.72 7.28a.75.75 0 1 1 1.06-1.06l4.998 5a.75.75 0 0 1 0 1.06l-4.998 5a.75.75 0 1 1-1.06-1.06l3.718-3.72H13.56l-4.78 4.78a.75.75 0 0 1-.531.22h-4.5a.75.75 0 0 1 0-1.5h4.19l4.25-4.25L7.94 7.5H3.75A.75.75 0 0 1 3 6.75Z", "more-horizontal-outline": "M7.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM13.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM18 13.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z", "more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z", @@ -68,22 +68,24 @@ "share-outline": "M6.747 4h3.464a.75.75 0 0 1 .102 1.493l-.102.007H6.747a2.25 2.25 0 0 0-2.245 2.096l-.005.154v9.5a2.25 2.25 0 0 0 2.096 2.245l.154.005h9.5a2.25 2.25 0 0 0 2.245-2.096l.005-.154v-.498a.75.75 0 0 1 1.494-.101l.006.101v.498a3.75 3.75 0 0 1-3.55 3.745l-.2.005h-9.5a3.75 3.75 0 0 1-3.745-3.55l-.005-.2v-9.5a3.75 3.75 0 0 1 3.55-3.745l.2-.005h3.464-3.464ZM14.5 6.52V3.75a.75.75 0 0 1 1.187-.61l.082.069 5.994 5.75c.28.268.306.7.077.997l-.077.085-5.994 5.752a.75.75 0 0 1-1.262-.434l-.007-.107v-2.725l-.344.03c-2.4.25-4.7 1.33-6.914 3.26-.52.453-1.323.025-1.237-.658.664-5.32 3.446-8.252 8.195-8.62l.3-.02V3.75v2.77ZM16 5.509V7.25a.75.75 0 0 1-.75.75c-3.874 0-6.274 1.676-7.312 5.157l-.079.279.352-.237C10.45 11.737 12.798 11 15.251 11a.75.75 0 0 1 .743.648l.007.102v1.743L20.16 9.5l-4.16-3.991Z", "sound-source-outline": "M3.5 12a8.5 8.5 0 1 1 14.762 5.748l.992 1.135A9.966 9.966 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12a9.966 9.966 0 0 0 2.746 6.883l.993-1.134A8.47 8.47 0 0 1 3.5 12Z M19.25 12.125a7.098 7.098 0 0 1-1.783 4.715l-.998-1.14a5.625 5.625 0 1 0-8.806-.15l-1.004 1.146a7.125 7.125 0 1 1 12.59-4.571Z M16.25 12a4.23 4.23 0 0 1-.821 2.511l-1.026-1.172a2.75 2.75 0 1 0-4.806 0L8.571 14.51A4.25 4.25 0 1 1 16.25 12Z M12.564 12.756a.75.75 0 0 0-1.128 0l-7 8A.75.75 0 0 0 5 22h14a.75.75 0 0 0 .564-1.244l-7-8Zm4.783 7.744H6.653L12 14.389l5.347 6.111Z", "speaker-1-outline": "M14.704 3.442c.191.226.296.512.296.808v15.502a1.25 1.25 0 0 1-2.058.954L7.975 16.5H4.25A2.25 2.25 0 0 1 2 14.25v-4.5A2.25 2.25 0 0 1 4.25 7.5h3.725l4.968-4.204a1.25 1.25 0 0 1 1.761.147ZM13.5 4.79 8.525 9H4.25a.75.75 0 0 0-.75.75v4.5c0 .415.336.75.75.75h4.275l4.975 4.213V4.79Zm3.604 3.851a.75.75 0 0 1 1.03.25c.574.94.862 1.992.862 3.14 0 1.149-.288 2.201-.862 3.141a.75.75 0 1 1-1.28-.781c.428-.702.642-1.483.642-2.36 0-.876-.214-1.657-.642-2.359a.75.75 0 0 1 .25-1.03Z", - "speaker-mute-outline" :"M12.92 3.316c.806-.717 2.08-.145 2.08.934v15.496c0 1.078-1.274 1.65-2.08.934l-4.492-3.994a.75.75 0 0 0-.498-.19H4.25A2.25 2.25 0 0 1 2 14.247V9.75a2.25 2.25 0 0 1 2.25-2.25h3.68a.75.75 0 0 0 .498-.19l4.491-3.993Zm.58 1.49L9.425 8.43A2.25 2.25 0 0 1 7.93 9H4.25a.75.75 0 0 0-.75.75v4.497c0 .415.336.75.75.75h3.68a2.25 2.25 0 0 1 1.495.57l4.075 3.623V4.807ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z", + "speaker-mute-outline": "M12.92 3.316c.806-.717 2.08-.145 2.08.934v15.496c0 1.078-1.274 1.65-2.08.934l-4.492-3.994a.75.75 0 0 0-.498-.19H4.25A2.25 2.25 0 0 1 2 14.247V9.75a2.25 2.25 0 0 1 2.25-2.25h3.68a.75.75 0 0 0 .498-.19l4.491-3.993Zm.58 1.49L9.425 8.43A2.25 2.25 0 0 1 7.93 9H4.25a.75.75 0 0 0-.75.75v4.497c0 .415.336.75.75.75h3.68a2.25 2.25 0 0 1 1.495.57l4.075 3.623V4.807ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z", "star-emphasis-outline": "M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z", "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" } diff --git a/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/countries.js b/app/javascript/shared/constants/countries.js similarity index 100% rename from app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/countries.js rename to app/javascript/shared/constants/countries.js diff --git a/app/services/contacts/filter_service.rb b/app/services/contacts/filter_service.rb index cefcee7b5..0b0ee57e9 100644 --- a/app/services/contacts/filter_service.rb +++ b/app/services/contacts/filter_service.rb @@ -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 diff --git a/app/views/api/v1/accounts/contacts/filter.json.jbuilder b/app/views/api/v1/accounts/contacts/filter.json.jbuilder index 87d953b5b..b3af9a8b7 100644 --- a/app/views/api/v1/accounts/contacts/filter.json.jbuilder +++ b/app/views/api/v1/accounts/contacts/filter.json.jbuilder @@ -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 diff --git a/lib/filters/filter_keys.json b/lib/filters/filter_keys.json index 5b2b63c69..9412980a7 100644 --- a/lib/filters/filter_keys.json +++ b/lib/filters/filter_keys.json @@ -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", From 6d378eb20640903c65feb26822ac1d45823bbf33 Mon Sep 17 00:00:00 2001 From: thedev105 <3080007+thedev105@users.noreply.github.com> Date: Fri, 3 Dec 2021 10:49:11 +0300 Subject: [PATCH 2/2] Fix: Update contact validation messages (#3500) Fixes #3476 --- .../api/v1/accounts/contacts_controller.rb | 5 ----- .../concerns/request_exception_handler.rb | 3 ++- .../dashboard/i18n/locale/en/contact.json | 7 ++++--- .../conversation/contact/ContactForm.vue | 17 +++++++---------- .../dashboard/store/modules/contacts/actions.js | 4 ++-- .../modules/specs/contacts/actions.spec.js | 3 ++- .../shared/helpers/specs/CustomErrors.spec.js | 8 ++------ .../api/v1/accounts/contacts_controller_spec.rb | 14 +++++++++++++- 8 files changed, 32 insertions(+), 29 deletions(-) diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index fcb1de831..76fd206f4 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -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 diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index 3ce486012..1f96de737 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -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 diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 018a2acc1..baa7c8d7d 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -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": { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue index 0fe9a1a4d..2b3dae832 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue @@ -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 { diff --git a/app/javascript/dashboard/store/modules/contacts/actions.js b/app/javascript/dashboard/store/modules/contacts/actions.js index 44bef1144..0e9961834 100644 --- a/app/javascript/dashboard/store/modules/contacts/actions.js +++ b/app/javascript/dashboard/store/modules/contacts/actions.js @@ -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); } diff --git a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js index 08b1b2067..b435318e3 100644 --- a/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/contacts/actions.spec.js @@ -94,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'], }, }, }); diff --git a/app/javascript/shared/helpers/specs/CustomErrors.spec.js b/app/javascript/shared/helpers/specs/CustomErrors.spec.js index 83b12da1d..cfa57dbce 100644 --- a/app/javascript/shared/helpers/specs/CustomErrors.spec.js +++ b/app/javascript/shared/helpers/specs/CustomErrors.spec.js @@ -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'], }); }); }); diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index b74ee44ed..6975fa015 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -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