feat: Add custom attribute table (#2929)

This commit is contained in:
Sivin Varghese 2021-09-08 09:37:58 +05:30 committed by GitHub
parent 39c4fa111a
commit c80289e661
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 242 additions and 42 deletions

View file

@ -1,9 +1,14 @@
/* global axios */
import ApiClient from './ApiClient';
class AttributeAPI extends ApiClient {
constructor() {
super('custom_attribute_definitions', { accountScoped: true });
}
getAttributesByModel(modelId) {
return axios.get(`${this.url}?attribute_model=${modelId}`);
}
}
export default new AttributeAPI();

View file

@ -2,6 +2,8 @@
"ATTRIBUTES_MGMT": {
"HEADER": "Attributes",
"HEADER_BTN_TXT": "Add Attribute",
"LOADING": "Fetching attributes",
"SIDEBAR_TXT": "<p><b>Attributes</b> <p>A custom attribute tracks facts about your contacts/conversation — like the subscription plan, or when they ordered the first item etc. <br /><br />For creating a Attributes, just click on the <b>Add Attribute.</b> You can also edit or delete an existing Attribute by clicking on the Edit or Delete button.</p>",
"ADD": {
"TITLE": "Add attribute",
"SUBMIT": "Create",
@ -9,11 +11,13 @@
"FORM": {
"NAME": {
"LABEL": "Display Name",
"PLACEHOLDER": "Enter attribute display name"
"PLACEHOLDER": "Enter attribute display name",
"ERROR": "Name is required"
},
"DESC": {
"LABEL": "Description",
"PLACEHOLDER": "Enter attribute description"
"PLACEHOLDER": "Enter attribute description",
"ERROR": "Description is required"
},
"MODEL": {
"LABEL": "Model",
@ -33,6 +37,22 @@
"SUCCESS_MESSAGE": "Attribute added successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
},
"TABS": {
"HEADER": "Custom Attributes",
"CONVERSATION": "Conversation",
"CONTACT": "Contact"
},
"LIST": {
"TABLE_HEADER": ["Name", "Description", "Type", "Key"],
"BUTTONS": {
"EDIT": "Edit",
"DELETE": "Delete"
},
"EMPTY_RESULT": {
"404": "There are no attributes created",
"NOT_FOUND": "There are no attributes configured"
}
}
}
}

View file

@ -5,17 +5,19 @@
<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">
<woot-input
v-model="displayName"
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL')"
type="text"
:class="{ error: $v.displayName.$error }"
:error="
$v.displayName.$error
? $t('ATTRIBUTES_MGMT.ADD.FORM.NAME.ERROR')
: ''
"
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
@blur="$v.displayName.$touch"
/>
<label :class="{ error: $v.description.$error }">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
<textarea
@ -25,9 +27,10 @@
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
@blur="$v.description.$touch"
/>
<span v-if="$v.description.$error" class="message">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.ERROR') }}
</span>
</label>
</div>
<div class="medium-12 columns">
<label :class="{ error: $v.attributeModel.$error }">
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.MODEL.LABEL') }}
<select v-model="attributeModel">
@ -39,8 +42,7 @@
{{ $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">
@ -52,18 +54,16 @@
{{ $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">
<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">
<woot-submit-button
:disabled="
$v.displayName.$invalid ||

View file

@ -0,0 +1,172 @@
<template>
<div class="row table-wrap">
<div class="column">
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
<woot-tabs-item
v-for="tab in tabs"
:key="tab.key"
:name="tab.name"
:show-badge="false"
/>
</woot-tabs>
<div class="columns with-right-space ">
<p
v-if="!uiFlags.isFetching && !attributes.length"
class="no-items-error-message"
>
{{ $t('ATTRIBUTES_MGMT.LIST.EMPTY_RESULT.404') }}
</p>
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('ATTRIBUTES_MGMT.LOADING')"
/>
<table
v-if="!uiFlags.isFetching && attributes.length"
class="woot-table"
>
<thead>
<th
v-for="tableHeader in $t('ATTRIBUTES_MGMT.LIST.TABLE_HEADER')"
:key="tableHeader"
class="item"
>
{{ tableHeader }}
</th>
</thead>
<tbody>
<tr v-for="attribute in attributes" :key="attribute.attribute_key">
<td class="item text-truncate">
{{ attribute.attribute_display_name }}
</td>
<td class="item-description text-truncate">
{{ attribute.attribute_description }}
</td>
<td class="item text-truncatee">
{{ attribute.attribute_display_type }}
</td>
<td class="item key text-truncate">
{{ attribute.attribute_key }}
</td>
<td class="button-wrapper">
<woot-button
variant="link"
color-scheme="secondary"
class-names="grey-btn"
icon="ion-edit"
>
{{ $t('ATTRIBUTES_MGMT.LIST.BUTTONS.EDIT') }}
</woot-button>
<woot-button
variant="link"
color-scheme="secondary"
icon="ion-close-circled"
class-names="grey-btn"
>
{{ $t('ATTRIBUTES_MGMT.LIST.BUTTONS.DELETE') }}
</woot-button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="small-4 columns">
<span v-html="$t('ATTRIBUTES_MGMT.SIDEBAR_TXT')"></span>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
selectedTabIndex: 0,
};
},
computed: {
...mapGetters({
uiFlags: 'attributes/getUIFlags',
}),
attributes() {
const attributeModel = this.selectedTabIndex
? 'contact_attribute'
: 'conversation_attribute';
return this.$store.getters['attributes/getAttributesByModel'](
attributeModel
);
},
tabs() {
return [
{
key: 0,
name: this.$t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
},
{
key: 1,
name: this.$t('ATTRIBUTES_MGMT.TABS.CONTACT'),
},
];
},
},
mounted() {
this.fetchAttributes(this.selectedTabIndex);
},
methods: {
onClickTabChange(index) {
this.selectedTabIndex = index;
this.fetchAttributes(index);
},
fetchAttributes(index) {
this.$store.dispatch('attributes/get', index);
},
},
};
</script>
<style lang="scss" scoped>
.table-wrap {
padding-left: var(--space-small);
}
.woot-table {
width: 100%;
margin-top: var(--space-small);
}
.no-items-error-message {
margin-top: var(--space-larger);
}
.tabs {
padding-left: 0;
margin-right: var(--space-medium);
user-select: none;
}
.item {
padding-left: 0;
max-width: 10rem;
min-width: 8rem;
}
.item-description {
padding-left: 0;
max-width: 16rem;
min-width: 10rem;
}
.key {
font-family: monospace;
}
::v-deep {
.tabs-title a {
font-weight: var(--font-weight-medium);
padding-top: 0;
}
}
</style>

View file

@ -8,6 +8,7 @@
>
{{ $t('ATTRIBUTES_MGMT.HEADER_BTN_TXT') }}
</woot-button>
<custom-attribute />
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-attribute :on-close="hideAddPopup" />
</woot-modal>
@ -16,9 +17,11 @@
<script>
import AddAttribute from './AddAttribute';
import CustomAttribute from './CustomAttribute';
export default {
components: {
AddAttribute,
CustomAttribute,
},
data() {
return {

View file

@ -14,18 +14,18 @@ export const getters = {
getUIFlags(_state) {
return _state.uiFlags;
},
getAttributes: _state => attributeType => {
getAttributesByModel: _state => attributeModel => {
return _state.records.filter(
record => record.attribute_display_type === attributeType
record => record.attribute_model === attributeModel
);
},
};
export const actions = {
get: async function getAttributes({ commit }) {
get: async function getAttributesByModel({ commit }, modelId) {
commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true });
try {
const response = await AttributeAPI.get();
const response = await AttributeAPI.getAttributesByModel(modelId);
commit(types.SET_CUSTOM_ATTRIBUTE, response.data);
} catch (error) {
// Ignore error

View file

@ -11,7 +11,7 @@ describe('#actions', () => {
describe('#get', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: attributesList });
await actions.get({ commit }, { inboxId: 23 });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true }],
[types.default.SET_CUSTOM_ATTRIBUTE, attributesList],
@ -20,7 +20,7 @@ describe('#actions', () => {
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await actions.get({ commit }, { inboxId: 23 });
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
[types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true }],
[types.default.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: false }],

View file

@ -1,16 +1,16 @@
export default [
{
attribute_display_name: 'Language',
attribute_display_type: 0,
attribute_display_type: 1,
attribute_description: 'The conversation language',
attribute_key: 'language',
attribute_model: 0,
},
{
attribute_display_name: 'Language one',
attribute_display_type: 1,
attribute_display_type: 2,
attribute_description: 'The conversation language one',
attribute_key: 'language_one',
attribute_model: 3,
attribute_model: 1,
},
];

View file

@ -2,15 +2,15 @@ import { getters } from '../../attributes';
import attributesList from './fixtures';
describe('#getters', () => {
it('getAttributes', () => {
it('getAttributesByModel', () => {
const state = { records: attributesList };
expect(getters.getAttributes(state)(1)).toEqual([
expect(getters.getAttributesByModel(state)(1)).toEqual([
{
attribute_display_name: 'Language one',
attribute_display_type: 1,
attribute_display_type: 2,
attribute_description: 'The conversation language one',
attribute_key: 'language_one',
attribute_model: 3,
attribute_model: 1,
},
]);
});