From fdcc322660731a4334865f09564ef21bdab4fa2f Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 31 Aug 2021 13:54:34 +0530 Subject: [PATCH] feat: Add the ability to create custom attribute (#2903) --- app/javascript/dashboard/api/attributes.js | 9 + .../i18n/locale/en/attributesMgmt.json | 34 ++- .../settings/attributes/AddAttribute.vue | 201 ++++++++++++++++++ .../dashboard/settings/attributes/Index.vue | 24 ++- app/javascript/dashboard/store/index.js | 2 + .../dashboard/store/modules/attributes.js | 91 ++++++++ .../modules/specs/attributes/actions.spec.js | 93 ++++++++ .../modules/specs/attributes/fixtures.js | 16 ++ .../modules/specs/attributes/getters.spec.js | 30 +++ .../specs/attributes/mutations.spec.js | 44 ++++ .../dashboard/store/mutation-types.js | 7 + app/models/custom_attribute_definition.rb | 2 +- 12 files changed, 550 insertions(+), 3 deletions(-) create mode 100644 app/javascript/dashboard/api/attributes.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue create mode 100644 app/javascript/dashboard/store/modules/attributes.js create mode 100644 app/javascript/dashboard/store/modules/specs/attributes/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/attributes/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/specs/attributes/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/attributes/mutations.spec.js diff --git a/app/javascript/dashboard/api/attributes.js b/app/javascript/dashboard/api/attributes.js new file mode 100644 index 000000000..c93807837 --- /dev/null +++ b/app/javascript/dashboard/api/attributes.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class AttributeAPI extends ApiClient { + constructor() { + super('custom_attribute_definitions', { accountScoped: true }); + } +} + +export default new AttributeAPI(); diff --git a/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json b/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json index af8c41e6f..275cdf81d 100644 --- a/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/attributesMgmt.json @@ -1,6 +1,38 @@ { "ATTRIBUTES_MGMT": { "HEADER": "Attributes", - "HEADER_BTN_TXT": "Add Attribute" + "HEADER_BTN_TXT": "Add Attribute", + "ADD": { + "TITLE": "Add attribute", + "SUBMIT": "Create", + "CANCEL_BUTTON_TEXT": "Cancel", + "FORM": { + "NAME": { + "LABEL": "Display Name", + "PLACEHOLDER": "Enter attribute display name" + }, + "DESC": { + "LABEL": "Description", + "PLACEHOLDER": "Enter attribute description" + }, + "MODEL": { + "LABEL": "Model", + "PLACEHOLDER": "Please select a model", + "ERROR": "Model is required" + }, + "TYPE": { + "LABEL": "Type", + "PLACEHOLDER": "Please select a type", + "ERROR": "Type is required" + }, + "KEY": { + "LABEL": "Key" + } + }, + "API": { + "SUCCESS_MESSAGE": "Attribute added successfully", + "ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later" + } + } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue b/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue new file mode 100644 index 000000000..ba00f54b1 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/AddAttribute.vue @@ -0,0 +1,201 @@ + + + + + + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL') }} + + + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }} + + + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }} + + + {{ model.option }} + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.ERROR') }} + + + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }} + + + {{ type.option }} + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }} + + + + + + {{ $t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL') }} + + + + {{ attributeKey }} + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue index 2a62e3363..a8491d315 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/attributes/Index.vue @@ -4,14 +4,36 @@ color-scheme="success" class-names="button--fixed-right-top" icon="ion-android-add-circle" + @click="openAddPopup()" > {{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }} + + + diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index fa5678eee..b14b9d0b0 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -30,6 +30,7 @@ import teamMembers from './modules/teamMembers'; import teams from './modules/teams'; import userNotificationSettings from './modules/userNotificationSettings'; import webhooks from './modules/webhooks'; +import attributes from './modules/attributes'; Vue.use(Vuex); export default new Vuex.Store({ @@ -63,5 +64,6 @@ export default new Vuex.Store({ teams, userNotificationSettings, webhooks, + attributes, }, }); diff --git a/app/javascript/dashboard/store/modules/attributes.js b/app/javascript/dashboard/store/modules/attributes.js new file mode 100644 index 000000000..669a22a6a --- /dev/null +++ b/app/javascript/dashboard/store/modules/attributes.js @@ -0,0 +1,91 @@ +import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; +import types from '../mutation-types'; +import AttributeAPI from '../../api/attributes'; + +export const state = { + records: [], + uiFlags: { + isFetching: false, + isCreating: false, + }, +}; + +export const getters = { + getUIFlags(_state) { + return _state.uiFlags; + }, + getAttributes: _state => attributeType => { + return _state.records.filter( + record => record.attribute_display_type === attributeType + ); + }, +}; + +export const actions = { + get: async function getAttributes({ commit }) { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true }); + try { + const response = await AttributeAPI.get(); + commit(types.SET_CUSTOM_ATTRIBUTE, response.data); + } catch (error) { + // Ignore error + } finally { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: false }); + } + }, + create: async function createAttribute({ commit }, attributeObj) { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isCreating: true }); + try { + const response = await AttributeAPI.create(attributeObj); + commit(types.ADD_CUSTOM_ATTRIBUTE, response.data); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isCreating: false }); + } + }, + update: async ({ commit }, { id, ...updateObj }) => { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isUpdating: true }); + try { + const response = await AttributeAPI.update(id, updateObj); + commit(types.EDIT_CUSTOM_ATTRIBUTE, response.data); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isUpdating: false }); + } + }, + delete: async ({ commit }, id) => { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isDeleting: true }); + try { + await AttributeAPI.delete(id); + commit(types.DELETE_CUSTOM_ATTRIBUTE, id); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isDeleting: false }); + } + }, +}; + +export const mutations = { + [types.SET_CUSTOM_ATTRIBUTE_UI_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, + + [types.ADD_CUSTOM_ATTRIBUTE]: MutationHelpers.create, + [types.SET_CUSTOM_ATTRIBUTE]: MutationHelpers.set, + [types.EDIT_CUSTOM_ATTRIBUTE]: MutationHelpers.update, + [types.DELETE_CUSTOM_ATTRIBUTE]: MutationHelpers.destroy, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/specs/attributes/actions.spec.js b/app/javascript/dashboard/store/modules/specs/attributes/actions.spec.js new file mode 100644 index 000000000..38ad67c86 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/attributes/actions.spec.js @@ -0,0 +1,93 @@ +import axios from 'axios'; +import { actions } from '../../attributes'; +import * as types from '../../../mutation-types'; +import attributesList from './fixtures'; + +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#get', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: attributesList }); + await actions.get({ commit }, { inboxId: 23 }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true }], + [types.default.SET_CUSTOM_ATTRIBUTE, attributesList], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.get({ commit }, { inboxId: 23 }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true }], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: false }], + ]); + }); + }); + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: attributesList[0] }); + await actions.create({ commit }, attributesList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isCreating: true }], + [types.default.ADD_CUSTOM_ATTRIBUTE, attributesList[0]], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isCreating: true }], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + axios.patch.mockResolvedValue({ data: attributesList[0] }); + await actions.update({ commit }, attributesList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isUpdating: true }], + [types.default.EDIT_CUSTOM_ATTRIBUTE, attributesList[0]], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isUpdating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.update({ commit }, attributesList[0]) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isUpdating: true }], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isUpdating: false }], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({ data: attributesList[0] }); + await actions.delete({ commit }, attributesList[0].id); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isDeleting: true }], + [types.default.DELETE_CUSTOM_ATTRIBUTE, attributesList[0].id], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isDeleting: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.delete({ commit }, attributesList[0].id) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isDeleting: true }], + [types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isDeleting: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/attributes/fixtures.js b/app/javascript/dashboard/store/modules/specs/attributes/fixtures.js new file mode 100644 index 000000000..1a5cb48e3 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/attributes/fixtures.js @@ -0,0 +1,16 @@ +export default [ + { + attribute_display_name: 'Language', + attribute_display_type: 0, + attribute_description: 'The conversation language', + attribute_key: 'language', + attribute_model: 0, + }, + { + attribute_display_name: 'Language one', + attribute_display_type: 1, + attribute_description: 'The conversation language one', + attribute_key: 'language_one', + attribute_model: 3, + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/attributes/getters.spec.js b/app/javascript/dashboard/store/modules/specs/attributes/getters.spec.js new file mode 100644 index 000000000..a6e94839c --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/attributes/getters.spec.js @@ -0,0 +1,30 @@ +import { getters } from '../../attributes'; +import attributesList from './fixtures'; + +describe('#getters', () => { + it('getAttributes', () => { + const state = { records: attributesList }; + expect(getters.getAttributes(state)(1)).toEqual([ + { + attribute_display_name: 'Language one', + attribute_display_type: 1, + attribute_description: 'The conversation language one', + attribute_key: 'language_one', + attribute_model: 3, + }, + ]); + }); + + it('getUIFlags', () => { + const state = { + uiFlags: { + isFetching: true, + isCreating: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isFetching: true, + isCreating: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/attributes/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/attributes/mutations.spec.js new file mode 100644 index 000000000..de3435c1c --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/attributes/mutations.spec.js @@ -0,0 +1,44 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../attributes'; +import attributesList from './fixtures'; + +describe('#mutations', () => { + describe('#SET_CUSTOM_ATTRIBUTE', () => { + it('set attribute records', () => { + const state = { records: [] }; + mutations[types.SET_CUSTOM_ATTRIBUTE](state, attributesList); + expect(state.records).toEqual(attributesList); + }); + }); + + describe('#ADD_CUSTOM_ATTRIBUTE', () => { + it('push newly created attributes to the store', () => { + const state = { records: [attributesList[0]] }; + mutations[types.ADD_CUSTOM_ATTRIBUTE](state, attributesList[1]); + expect(state.records).toEqual([attributesList[0], attributesList[1]]); + }); + }); + describe('#EDIT_CUSTOM_ATTRIBUTE', () => { + it('update attribute record', () => { + const state = { records: [attributesList[0]] }; + mutations[types.EDIT_CUSTOM_ATTRIBUTE](state, { + attribute_display_name: 'Language', + attribute_display_type: 0, + attribute_description: 'The conversation language', + attribute_key: 'language', + attribute_model: 0, + }); + expect(state.records[0].attribute_description).toEqual( + 'The conversation language' + ); + }); + }); + + describe('#DELETE_CUSTOM_ATTRIBUTE', () => { + it('delete attribute record', () => { + const state = { records: [attributesList[0]] }; + mutations[types.DELETE_CUSTOM_ATTRIBUTE](state, attributesList[0]); + expect(state.records).toEqual([attributesList[0]]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index df0f8fff6..c364f7e4d 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -172,4 +172,11 @@ export default { SET_CSAT_RESPONSE_UI_FLAG: 'SET_CSAT_RESPONSE_UI_FLAG', SET_CSAT_RESPONSE: 'SET_CSAT_RESPONSE', SET_CSAT_RESPONSE_METRICS: 'SET_CSAT_RESPONSE_METRICS', + + // Custom Attributes + SET_CUSTOM_ATTRIBUTE_UI_FLAG: 'SET_CUSTOM_ATTRIBUTE_UI_FLAG', + SET_CUSTOM_ATTRIBUTE: 'SET_CUSTOM_ATTRIBUTE', + ADD_CUSTOM_ATTRIBUTE: 'ADD_CUSTOM_ATTRIBUTE', + EDIT_CUSTOM_ATTRIBUTE: 'EDIT_CUSTOM_ATTRIBUTE', + DELETE_CUSTOM_ATTRIBUTE: 'DELETE_CUSTOM_ATTRIBUTE', }; diff --git a/app/models/custom_attribute_definition.rb b/app/models/custom_attribute_definition.rb index 3c861696f..1e43b5e09 100644 --- a/app/models/custom_attribute_definition.rb +++ b/app/models/custom_attribute_definition.rb @@ -29,7 +29,7 @@ class CustomAttributeDefinition < ApplicationRecord validates :attribute_model, presence: true enum attribute_model: { conversation_attribute: 0, contact_attribute: 1 } - enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4 } + enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5 } belongs_to :account end
+ {{ attributeKey }} +