mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: Add the ability to create custom attribute (#2903)
This commit is contained in:
		
							
								
								
									
										9
									
								
								app/javascript/dashboard/api/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/javascript/dashboard/api/attributes.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import ApiClient from './ApiClient';
 | 
			
		||||
 | 
			
		||||
class AttributeAPI extends ApiClient {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super('custom_attribute_definitions', { accountScoped: true });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new AttributeAPI();
 | 
			
		||||
@@ -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"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,201 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <woot-modal :show.sync="show" :on-close="onClose">
 | 
			
		||||
    <div class="column content-box">
 | 
			
		||||
      <woot-modal-header :header-title="$t('ATTRIBUTES_MGMT.ADD.TITLE')" />
 | 
			
		||||
 | 
			
		||||
      <form class="row" @submit.prevent="addAttributes()">
 | 
			
		||||
        <div class="medium-12 columns">
 | 
			
		||||
          <label :class="{ error: $v.displayName.$error }">
 | 
			
		||||
            {{ $t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL') }}
 | 
			
		||||
            <input
 | 
			
		||||
              v-model.trim="displayName"
 | 
			
		||||
              type="text"
 | 
			
		||||
              :placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
 | 
			
		||||
              @blur="$v.displayName.$touch"
 | 
			
		||||
            />
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="medium-12 columns">
 | 
			
		||||
          <label :class="{ error: $v.description.$error }">
 | 
			
		||||
            {{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
 | 
			
		||||
            <textarea
 | 
			
		||||
              v-model="description"
 | 
			
		||||
              rows="5"
 | 
			
		||||
              type="text"
 | 
			
		||||
              :placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
 | 
			
		||||
              @blur="$v.description.$touch"
 | 
			
		||||
            />
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="medium-12 columns">
 | 
			
		||||
          <label :class="{ error: $v.attributeModel.$error }">
 | 
			
		||||
            {{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }}
 | 
			
		||||
            <select v-model="attributeModel">
 | 
			
		||||
              <option v-for="model in models" :key="model.id" :value="model.id">
 | 
			
		||||
                {{ model.option }}
 | 
			
		||||
              </option>
 | 
			
		||||
            </select>
 | 
			
		||||
            <span v-if="$v.attributeModel.$error" class="message">
 | 
			
		||||
              {{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.ERROR') }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="medium-12 columns">
 | 
			
		||||
          <label :class="{ error: $v.attributeType.$error }">
 | 
			
		||||
            {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }}
 | 
			
		||||
            <select v-model="attributeType">
 | 
			
		||||
              <option v-for="type in types" :key="type.id" :value="type.id">
 | 
			
		||||
                {{ type.option }}
 | 
			
		||||
              </option>
 | 
			
		||||
            </select>
 | 
			
		||||
            <span v-if="$v.attributeType.$error" class="message">
 | 
			
		||||
              {{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
 | 
			
		||||
            </span>
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="displayName" class="medium-12 columns">
 | 
			
		||||
          <label>
 | 
			
		||||
            {{ $t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL') }}
 | 
			
		||||
            <i class="ion-help" />
 | 
			
		||||
          </label>
 | 
			
		||||
          <p class="key-value text-truncate">
 | 
			
		||||
            {{ attributeKey }}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="modal-footer">
 | 
			
		||||
          <div class="medium-12 columns">
 | 
			
		||||
            <woot-submit-button
 | 
			
		||||
              :disabled="
 | 
			
		||||
                $v.displayName.$invalid ||
 | 
			
		||||
                  $v.description.$invalid ||
 | 
			
		||||
                  uiFlags.isCreating
 | 
			
		||||
              "
 | 
			
		||||
              :button-text="$t('ATTRIBUTES_MGMT.ADD.SUBMIT')"
 | 
			
		||||
            />
 | 
			
		||||
            <button class="button clear" @click.prevent="onClose">
 | 
			
		||||
              {{ $t('ATTRIBUTES_MGMT.ADD.CANCEL_BUTTON_TEXT') }}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </woot-modal>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { required, minLength } from 'vuelidate/lib/validators';
 | 
			
		||||
import { mapGetters } from 'vuex';
 | 
			
		||||
import alertMixin from 'shared/mixins/alertMixin';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  mixins: [alertMixin],
 | 
			
		||||
  props: {
 | 
			
		||||
    onClose: {
 | 
			
		||||
      type: Function,
 | 
			
		||||
      default: () => {},
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      displayName: '',
 | 
			
		||||
      description: '',
 | 
			
		||||
      attributeModel: 0,
 | 
			
		||||
      attributeType: 0,
 | 
			
		||||
      models: [
 | 
			
		||||
        {
 | 
			
		||||
          id: 0,
 | 
			
		||||
          option: 'Conversation',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 1,
 | 
			
		||||
          option: 'Contact',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      types: [
 | 
			
		||||
        {
 | 
			
		||||
          id: 0,
 | 
			
		||||
          option: 'Text',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 1,
 | 
			
		||||
          option: 'Number',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 2,
 | 
			
		||||
          option: 'Currency',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 3,
 | 
			
		||||
          option: 'Percent',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 4,
 | 
			
		||||
          option: 'Link',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 5,
 | 
			
		||||
          option: 'Date',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      show: true,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      uiFlags: 'getUIFlags',
 | 
			
		||||
    }),
 | 
			
		||||
    attributeKey() {
 | 
			
		||||
      return this.displayName
 | 
			
		||||
        .toLowerCase()
 | 
			
		||||
        .replace(/[^\w ]+/g, '')
 | 
			
		||||
        .replace(/ +/g, '_');
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  validations: {
 | 
			
		||||
    displayName: {
 | 
			
		||||
      required,
 | 
			
		||||
      minLength: minLength(1),
 | 
			
		||||
    },
 | 
			
		||||
    description: {
 | 
			
		||||
      required,
 | 
			
		||||
    },
 | 
			
		||||
    attributeModel: {
 | 
			
		||||
      required,
 | 
			
		||||
    },
 | 
			
		||||
    attributeType: {
 | 
			
		||||
      required,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    async addAttributes() {
 | 
			
		||||
      try {
 | 
			
		||||
        await this.$store.dispatch('attributes/create', {
 | 
			
		||||
          attribute_display_name: this.displayName,
 | 
			
		||||
          attribute_description: this.description,
 | 
			
		||||
          attribute_model: this.attributeModel,
 | 
			
		||||
          attribute_display_type: this.attributeType,
 | 
			
		||||
          attribute_key: this.attributeKey,
 | 
			
		||||
        });
 | 
			
		||||
        this.alertMessage = this.$t('ATTRIBUTES_MGMT.ADD.API.SUCCESS_MESSAGE');
 | 
			
		||||
        this.onClose();
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        const errorMessage = error?.response?.data?.message;
 | 
			
		||||
        this.alertMessage =
 | 
			
		||||
          errorMessage || this.$t('ATTRIBUTES_MGMT.ADD.API.ERROR_MESSAGE');
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.showAlert(this.alertMessage);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.key-value {
 | 
			
		||||
  padding: 0 var(--space-small) var(--space-small) 0;
 | 
			
		||||
  font-family: monospace;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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') }}
 | 
			
		||||
    </woot-button>
 | 
			
		||||
    <woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
 | 
			
		||||
      <add-attribute :on-close="hideAddPopup" />
 | 
			
		||||
    </woot-modal>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {};
 | 
			
		||||
import AddAttribute from './AddAttribute';
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    AddAttribute,
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showAddPopup: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    openAddPopup() {
 | 
			
		||||
      this.showAddPopup = true;
 | 
			
		||||
    },
 | 
			
		||||
    hideAddPopup() {
 | 
			
		||||
      this.showAddPopup = false;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										91
									
								
								app/javascript/dashboard/store/modules/attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/javascript/dashboard/store/modules/attributes.js
									
									
									
									
									
										Normal file
									
								
							@@ -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,
 | 
			
		||||
};
 | 
			
		||||
@@ -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 }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
@@ -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,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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]]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -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',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user