From ab77e03c922e06ab5c5cf9faf83b2bd0e31ca868 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Sat, 30 Oct 2021 07:09:46 +0530 Subject: [PATCH] feat: Render conversation custom attributes (#3065) --- .codeclimate.yml | 6 +- .../dashboard/api/inbox/conversation.js | 6 + .../api/specs/inbox/conversation.spec.js | 13 ++ .../dashboard/assets/scss/widgets/_forms.scss | 7 +- .../components/Accordion/AccordionItem.vue | 14 +- .../dashboard/components/CustomAttribute.vue | 220 ++++++++++++++++++ .../i18n/locale/en/attributesMgmt.json | 4 +- .../dashboard/i18n/locale/en/contact.json | 29 +++ .../i18n/locale/en/conversation.json | 21 ++ .../dashboard/mixins/attributeMixin.js | 56 +++++ .../mixins/specs/attributeFixtures.js | 26 +++ .../mixins/specs/attributeMixin.spec.js | 89 +++++++ .../conversation/ContactConversations.vue | 4 - .../conversation/ContactCustomAttributes.vue | 4 - .../conversation/ContactDetailsItem.vue | 32 +-- .../dashboard/conversation/ContactPanel.vue | 142 +++-------- .../conversation/ConversationInfo.vue | 144 ++++++++++++ .../conversation/contact/ContactInfo.vue | 31 ++- .../CustomAttributeDropDown.vue | 155 ++++++++++++ .../CustomAttributeDropDownItem.vue | 79 +++++++ .../CustomAttributeSelector.vue | 120 ++++++++++ .../customAttributes/CustomAttributes.vue | 101 ++++++++ .../settings/attributes/AddAttribute.vue | 27 ++- .../settings/attributes/CustomAttribute.vue | 6 +- .../settings/attributes/constants.js | 12 - .../dashboard/settings/settings.routes.js | 2 + .../store/modules/conversations/actions.js | 19 ++ .../store/modules/conversations/index.js | 8 + .../specs/conversations/actions.spec.js | 21 ++ .../specs/conversations/mutations.spec.js | 14 ++ .../dashboard/store/mutation-types.js | 2 + app/javascript/shared/constants/busEvents.js | 1 + 32 files changed, 1233 insertions(+), 182 deletions(-) create mode 100644 app/javascript/dashboard/components/CustomAttribute.vue create mode 100644 app/javascript/dashboard/mixins/attributeMixin.js create mode 100644 app/javascript/dashboard/mixins/specs/attributeFixtures.js create mode 100644 app/javascript/dashboard/mixins/specs/attributeMixin.spec.js create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/ConversationInfo.vue create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeDropDown.vue create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeDropDownItem.vue create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeSelector.vue create mode 100644 app/javascript/dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue diff --git a/.codeclimate.yml b/.codeclimate.yml index 50d5360fd..667b0f340 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -17,7 +17,11 @@ checks: method-count: enabled: true config: - threshold: 25 + threshold: 30 + file-lines: + enabled: true + config: + threshold: 300 exclude_patterns: - "spec/" - "**/specs/" diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index d5599ade0..e407aecb0 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -80,6 +80,12 @@ class ConversationApi extends ApiClient { sendEmailTranscript({ conversationId, email }) { return axios.post(`${this.url}/${conversationId}/transcript`, { email }); } + + updateCustomAttributes({ conversationId, customAttributes }) { + return axios.post(`${this.url}/${conversationId}/custom_attributes`, { + custom_attributes: customAttributes, + }); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index f068b4a4f..814b83446 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -160,5 +160,18 @@ describe('#ConversationAPI', () => { } ); }); + + it('#updateCustomAttributes', () => { + conversationAPI.updateCustomAttributes({ + conversationId: 45, + customAttributes: { order_d: '1001' }, + }); + expect(context.axiosMock.post).toHaveBeenCalledWith( + '/api/v1/conversations/45/custom_attributes', + { + custom_attributes: { order_d: '1001' }, + } + ); + }); }); }); diff --git a/app/javascript/dashboard/assets/scss/widgets/_forms.scss b/app/javascript/dashboard/assets/scss/widgets/_forms.scss index 17a7acaa8..13548db5f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_forms.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_forms.scss @@ -1,8 +1,7 @@ .error { - #{$all-text-inputs}, select, - .multiselect>.multiselect__tags { + .multiselect > .multiselect__tags { @include thin-border(var(--r-400)); } @@ -40,4 +39,8 @@ input { font-size: var(--font-size-small); height: var(--space-large); } + + .error { + border-color: var(--r-400); + } } diff --git a/app/javascript/dashboard/components/Accordion/AccordionItem.vue b/app/javascript/dashboard/components/Accordion/AccordionItem.vue index c162ee313..bfe817285 100644 --- a/app/javascript/dashboard/components/Accordion/AccordionItem.vue +++ b/app/javascript/dashboard/components/Accordion/AccordionItem.vue @@ -15,7 +15,11 @@ -
+
@@ -33,6 +37,10 @@ export default { type: String, required: true, }, + compact: { + type: Boolean, + default: false, + }, icon: { type: String, default: '', @@ -106,5 +114,9 @@ export default { .cw-accordion--content { padding: var(--space-normal); + + &.compact { + padding: 0; + } } diff --git a/app/javascript/dashboard/components/CustomAttribute.vue b/app/javascript/dashboard/components/CustomAttribute.vue new file mode 100644 index 000000000..0f5afbb88 --- /dev/null +++ b/app/javascript/dashboard/components/CustomAttribute.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json b/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json index d7bcb597a..f31070f52 100644 --- a/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json @@ -20,8 +20,8 @@ "ERROR": "Description is required" }, "MODEL": { - "LABEL": "Model", - "PLACEHOLDER": "Please select a model", + "LABEL": "Applies to", + "PLACEHOLDER": "Please select one", "ERROR": "Model is required" }, "TYPE": { diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index b0f4eb9ae..6d2b26443 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -237,8 +237,15 @@ } }, "CUSTOM_ATTRIBUTES": { + "ADD_BUTTON_TEXT": "Add attributes", "BUTTON": "Add custom attribute", "NOT_AVAILABLE": "There are no custom attributes available for this contact.", + "COPY_SUCCESSFUL": "Copied to clipboard successfully", + "ACTIONS": { + "COPY": "Copy attribute", + "DELETE": "Delete attribute", + "EDIT": "Edit attribute" + }, "ADD": { "TITLE": "Create custom attribute", "DESC": "Add custom information to this contact." @@ -254,7 +261,29 @@ "VALUE": { "LABEL": "Attribute value", "PLACEHOLDER": "Eg: 11901 " + }, + "ADD": { + "TITLE": "Add", + "SUCCESS": "Attribute added successfully", + "ERROR": "Unable to add attribute. Please try again later" + }, + "UPDATE": { + "SUCCESS": "Attribute updated successfully", + "ERROR": "Unable to update attribute. Please try again later" + }, + "DELETE": { + "SUCCESS": "Attribute deleted successfully", + "ERROR": "Unable to delete attribute. Please try again later" + }, + "ATTRIBUTE_SELECT": { + "TITLE": "Add attributes", + "PLACEHOLDER": "Search attributes", + "NO_RESULT": "No attributes found" } + }, + "VALIDATIONS": { + "REQUIRED": "Valid value is required", + "INVALID_URL": "Invalid URL" } }, "MERGE_CONTACTS": { diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 2eca31606..a50e8cb16 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -156,6 +156,27 @@ "PREVIOUS_CONVERSATION": "Previous Conversations" } }, + "CONVERSATION_CUSTOM_ATTRIBUTES": { + "ADD_BUTTON_TEXT": "Create attribute", + "UPDATE": { + "SUCCESS": "Attribute updated successfully", + "ERROR": "Unable to update attribute. Please try again later" + }, + "ADD": { + "TITLE": "Add", + "SUCCESS": "Attribute added successfully", + "ERROR": "Unable to add attribute. Please try again later" + }, + "DELETE": { + "SUCCESS": "Attribute deleted successfully", + "ERROR": "Unable to delete attribute. Please try again later" + }, + "ATTRIBUTE_SELECT": { + "TITLE": "Add attributes", + "PLACEHOLDER": "Search attributes", + "NO_RESULT": "No attributes found" + } + }, "EMAIL_HEADER": { "TO": "To", "BCC": "Bcc", diff --git a/app/javascript/dashboard/mixins/attributeMixin.js b/app/javascript/dashboard/mixins/attributeMixin.js new file mode 100644 index 000000000..5ed944e77 --- /dev/null +++ b/app/javascript/dashboard/mixins/attributeMixin.js @@ -0,0 +1,56 @@ +import { mapGetters } from 'vuex'; + +export default { + computed: { + ...mapGetters({ + currentChat: 'getSelectedChat', + accountId: 'getCurrentAccountId', + }), + attributes() { + return this.$store.getters['attributes/getAttributesByModel']( + 'conversation_attribute' + ); + }, + customAttributes() { + return this.currentChat.custom_attributes || {}; + }, + conversationId() { + return this.currentChat.id; + }, + // Select only custom attribute which are already defined + filteredAttributes() { + return Object.keys(this.customAttributes) + .filter(key => { + return this.attributes.find(item => item.attribute_key === key); + }) + .map(key => { + const item = this.attributes.find( + attribute => attribute.attribute_key === key + ); + return { + ...item, + value: this.customAttributes[key], + icon: this.attributeIcon(item.attribute_display_type), + }; + }); + }, + }, + methods: { + attributeIcon(attributeType) { + switch (attributeType) { + case 'date': + return 'ion-calendar'; + case 'link': + return 'ion-link'; + case 'currency': + return 'ion-social-usd'; + case 'number': + return 'ion-calculator'; + case 'percent': + return 'ion-calculator'; + default: + return 'ion-edit'; + } + }, + }, +}; diff --git a/app/javascript/dashboard/mixins/specs/attributeFixtures.js b/app/javascript/dashboard/mixins/specs/attributeFixtures.js new file mode 100644 index 000000000..9b0cc0fc1 --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/attributeFixtures.js @@ -0,0 +1,26 @@ +export default [ + { + attribute_description: 'Product name', + attribute_display_name: 'Product name', + attribute_display_type: 'text', + attribute_key: 'product_name', + attribute_model: 'conversation_attribute', + created_at: '2021-09-03T10:45:09.587Z', + default_value: null, + id: 6, + updated_at: '2021-09-22T10:40:42.511Z', + }, + { + attribute_description: 'Product identifier', + attribute_display_name: 'Product id', + attribute_display_type: 'number', + attribute_key: 'product_id', + attribute_model: 'conversation_attribute', + created_at: '2021-09-16T13:06:47.329Z', + default_value: null, + icon: 'ion-calculator', + id: 10, + updated_at: '2021-09-22T10:42:25.873Z', + value: 2021, + }, +]; diff --git a/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js b/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js new file mode 100644 index 000000000..c783b3bef --- /dev/null +++ b/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js @@ -0,0 +1,89 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import attributeMixin from '../attributeMixin'; +import Vuex from 'vuex'; +import attributeFixtures from './attributeFixtures'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('attributeMixin', () => { + let getters; + let actions; + let store; + + beforeEach(() => { + actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() }; + getters = { + getSelectedChat: () => ({ + id: 7165, + custom_attributes: { + product_id: 2021, + }, + }), + getCurrentAccountId: () => 1, + }; + store = new Vuex.Store({ actions, getters }); + }); + + it('returns currently selected conversation custom attributes', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [attributeMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.customAttributes).toEqual({ + product_id: 2021, + }); + }); + + it('returns currently selected conversation id', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [attributeMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.conversationId).toEqual(7165); + }); + + it('returns filtered attributes from conversation custom attributes', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [attributeMixin], + computed: { + attributes() { + return attributeFixtures; + }, + }, + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.filteredAttributes).toEqual([ + { + attribute_description: 'Product identifier', + attribute_display_name: 'Product id', + attribute_display_type: 'number', + attribute_key: 'product_id', + attribute_model: 'conversation_attribute', + created_at: '2021-09-16T13:06:47.329Z', + default_value: null, + icon: 'ion-calculator', + id: 10, + updated_at: '2021-09-22T10:42:25.873Z', + value: 2021, + }, + ]); + }); + + it('return icon if attribute type passed correctly', () => { + const Component = { + render() {}, + title: 'TestComponent', + mixins: [attributeMixin], + }; + const wrapper = shallowMount(Component, { store, localVue }); + expect(wrapper.vm.attributeIcon('date')).toBe('ion-calendar'); + expect(wrapper.vm.attributeIcon()).toBe('ion-edit'); + }); +}); diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue index 8de144be7..c4a4b9117 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactConversations.vue @@ -74,8 +74,4 @@ export default { margin-bottom: var(--space-normal); color: var(--b-500); } - -.conv-details--item { - padding-bottom: 0; -} diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue index 0b2749700..6f73aa51e 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactCustomAttributes.vue @@ -61,10 +61,6 @@ export default { margin-bottom: var(--space-normal); } -.conv-details--item { - padding-bottom: 0; -} - .custom-attribute--row__attribute { font-weight: 500; } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue index f84f0e3fb..c2987fe9c 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue @@ -1,9 +1,8 @@ @@ -34,6 +29,12 @@ export default { diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index 719e1b64e..3a9329b71 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -13,6 +13,7 @@