feat: Render conversation custom attributes (#3065)
This commit is contained in:
parent
69f55a25b6
commit
ab77e03c92
32 changed files with 1233 additions and 182 deletions
|
@ -17,7 +17,11 @@ checks:
|
||||||
method-count:
|
method-count:
|
||||||
enabled: true
|
enabled: true
|
||||||
config:
|
config:
|
||||||
threshold: 25
|
threshold: 30
|
||||||
|
file-lines:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
threshold: 300
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- "spec/"
|
- "spec/"
|
||||||
- "**/specs/"
|
- "**/specs/"
|
||||||
|
|
|
@ -80,6 +80,12 @@ class ConversationApi extends ApiClient {
|
||||||
sendEmailTranscript({ conversationId, email }) {
|
sendEmailTranscript({ conversationId, email }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/transcript`, { 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();
|
export default new ConversationApi();
|
||||||
|
|
|
@ -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' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
.error {
|
.error {
|
||||||
|
|
||||||
#{$all-text-inputs},
|
#{$all-text-inputs},
|
||||||
select,
|
select,
|
||||||
.multiselect>.multiselect__tags {
|
.multiselect > .multiselect__tags {
|
||||||
@include thin-border(var(--r-400));
|
@include thin-border(var(--r-400));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,4 +39,8 @@ input {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
height: var(--space-large);
|
height: var(--space-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
border-color: var(--r-400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="isOpen" class="cw-accordion--content">
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="cw-accordion--content"
|
||||||
|
:class="{ compact: compact }"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,6 +37,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
compact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -106,5 +114,9 @@ export default {
|
||||||
|
|
||||||
.cw-accordion--content {
|
.cw-accordion--content {
|
||||||
padding: var(--space-normal);
|
padding: var(--space-normal);
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
220
app/javascript/dashboard/components/CustomAttribute.vue
Normal file
220
app/javascript/dashboard/components/CustomAttribute.vue
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<div class="contact-attribute">
|
||||||
|
<div class="title-wrap">
|
||||||
|
<h4 class="text-block-title title error">
|
||||||
|
<span class="attribute-name" :class="{ error: $v.editedValue.$error }">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div v-show="isEditing">
|
||||||
|
<div class="input-group small">
|
||||||
|
<input
|
||||||
|
ref="inputfield"
|
||||||
|
v-model="editedValue"
|
||||||
|
:type="inputType"
|
||||||
|
class="input-group-field"
|
||||||
|
autofocus="true"
|
||||||
|
:class="{ error: $v.editedValue.$error }"
|
||||||
|
@blur="$v.editedValue.$touch"
|
||||||
|
@keyup.enter="onUpdate"
|
||||||
|
/>
|
||||||
|
<div class="input-group-button">
|
||||||
|
<woot-button size="small" icon="ion-checkmark" @click="onUpdate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="shouldShowErrorMessage" class="error-message">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="!isEditing"
|
||||||
|
class="value--view"
|
||||||
|
:class="{ 'is-editable': showActions }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="isAttributeTypeLink"
|
||||||
|
:href="value"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="value"
|
||||||
|
>
|
||||||
|
{{ value || '---' }}
|
||||||
|
</a>
|
||||||
|
<p v-else class="value">
|
||||||
|
{{ value || '---' }}
|
||||||
|
</p>
|
||||||
|
<woot-button
|
||||||
|
v-if="showActions"
|
||||||
|
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||||
|
variant="link"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="ion-clipboard"
|
||||||
|
class-names="edit-button"
|
||||||
|
@click="onCopy"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="showActions"
|
||||||
|
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||||
|
variant="link"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="ion-compose"
|
||||||
|
class-names="edit-button"
|
||||||
|
@click="onEdit"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="showActions"
|
||||||
|
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||||
|
variant="link"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="ion-trash-a"
|
||||||
|
class-names="edit-button"
|
||||||
|
@click="onDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { required, url } from 'vuelidate/lib/validators';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
label: { type: String, required: true },
|
||||||
|
value: { type: [String, Number], default: '' },
|
||||||
|
showActions: { type: Boolean, default: false },
|
||||||
|
attributeType: { type: String, default: 'text' },
|
||||||
|
attributeKey: { type: String, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEditing: false,
|
||||||
|
editedValue: this.value,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations() {
|
||||||
|
if (this.isAttributeTypeLink) {
|
||||||
|
return {
|
||||||
|
editedValue: { required, url },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
editedValue: { required },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isAttributeTypeLink() {
|
||||||
|
return this.attributeType === 'link';
|
||||||
|
},
|
||||||
|
inputType() {
|
||||||
|
return this.isAttributeTypeLink ? 'url' : this.attributeType;
|
||||||
|
},
|
||||||
|
shouldShowErrorMessage() {
|
||||||
|
return this.$v.editedValue.$error;
|
||||||
|
},
|
||||||
|
errorMessage() {
|
||||||
|
if (this.$v.editedValue.url) {
|
||||||
|
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.INVALID_URL');
|
||||||
|
}
|
||||||
|
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
||||||
|
if (this.attributeKey === focusAttributeKey) {
|
||||||
|
this.onEdit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focusInput() {
|
||||||
|
if (this.$refs.inputfield) {
|
||||||
|
this.$refs.inputfield.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEdit() {
|
||||||
|
this.isEditing = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.focusInput();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onUpdate() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isEditing = false;
|
||||||
|
this.$emit('update', this.attributeKey, this.editedValue);
|
||||||
|
},
|
||||||
|
onDelete() {
|
||||||
|
this.isEditing = false;
|
||||||
|
this.$emit('delete', this.attributeKey);
|
||||||
|
},
|
||||||
|
onCopy() {
|
||||||
|
this.$emit('copy', this.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.contact-attribute {
|
||||||
|
padding: var(--space-slab) var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-mini);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.attribute-name {
|
||||||
|
&.error {
|
||||||
|
color: var(--r-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title--icon {
|
||||||
|
width: var(--space-two);
|
||||||
|
}
|
||||||
|
.edit-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.value--view {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.is-editable:hover {
|
||||||
|
.value {
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
.edit-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: var(--space-mega);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
word-break: break-all;
|
||||||
|
padding: var(--space-micro) var(--space-smaller);
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
color: var(--r-400);
|
||||||
|
display: block;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-top: -1.6rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -20,8 +20,8 @@
|
||||||
"ERROR": "Description is required"
|
"ERROR": "Description is required"
|
||||||
},
|
},
|
||||||
"MODEL": {
|
"MODEL": {
|
||||||
"LABEL": "Model",
|
"LABEL": "Applies to",
|
||||||
"PLACEHOLDER": "Please select a model",
|
"PLACEHOLDER": "Please select one",
|
||||||
"ERROR": "Model is required"
|
"ERROR": "Model is required"
|
||||||
},
|
},
|
||||||
"TYPE": {
|
"TYPE": {
|
||||||
|
|
|
@ -237,8 +237,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CUSTOM_ATTRIBUTES": {
|
"CUSTOM_ATTRIBUTES": {
|
||||||
|
"ADD_BUTTON_TEXT": "Add attributes",
|
||||||
"BUTTON": "Add custom attribute",
|
"BUTTON": "Add custom attribute",
|
||||||
"NOT_AVAILABLE": "There are no custom attributes available for this contact.",
|
"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": {
|
"ADD": {
|
||||||
"TITLE": "Create custom attribute",
|
"TITLE": "Create custom attribute",
|
||||||
"DESC": "Add custom information to this contact."
|
"DESC": "Add custom information to this contact."
|
||||||
|
@ -254,7 +261,29 @@
|
||||||
"VALUE": {
|
"VALUE": {
|
||||||
"LABEL": "Attribute value",
|
"LABEL": "Attribute value",
|
||||||
"PLACEHOLDER": "Eg: 11901 "
|
"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": {
|
"MERGE_CONTACTS": {
|
||||||
|
|
|
@ -156,6 +156,27 @@
|
||||||
"PREVIOUS_CONVERSATION": "Previous Conversations"
|
"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": {
|
"EMAIL_HEADER": {
|
||||||
"TO": "To",
|
"TO": "To",
|
||||||
"BCC": "Bcc",
|
"BCC": "Bcc",
|
||||||
|
|
56
app/javascript/dashboard/mixins/attributeMixin.js
Normal file
56
app/javascript/dashboard/mixins/attributeMixin.js
Normal file
|
@ -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';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
26
app/javascript/dashboard/mixins/specs/attributeFixtures.js
Normal file
26
app/javascript/dashboard/mixins/specs/attributeFixtures.js
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
];
|
89
app/javascript/dashboard/mixins/specs/attributeMixin.spec.js
Normal file
89
app/javascript/dashboard/mixins/specs/attributeMixin.spec.js
Normal file
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -74,8 +74,4 @@ export default {
|
||||||
margin-bottom: var(--space-normal);
|
margin-bottom: var(--space-normal);
|
||||||
color: var(--b-500);
|
color: var(--b-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-details--item {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -61,10 +61,6 @@ export default {
|
||||||
margin-bottom: var(--space-normal);
|
margin-bottom: var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-details--item {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-attribute--row__attribute {
|
.custom-attribute--row__attribute {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="conv-details--item">
|
<div class="conv-details--item" :class="{ compact: compact }">
|
||||||
<h4 class="conv-details--item__label text-block-title">
|
<h4 class="conv-details--item__label text-block-title">
|
||||||
<span class="title--icon">
|
<span class="item__title">
|
||||||
<emoji-or-icon :icon="icon" :emoji="emoji" />
|
{{ title }}
|
||||||
<span>{{ title }}</span>
|
|
||||||
</span>
|
</span>
|
||||||
<slot name="button"></slot>
|
<slot name="button"></slot>
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -16,17 +15,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
EmojiOrIcon,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
icon: { type: String, default: '' },
|
icon: { type: String, default: '' },
|
||||||
emoji: { type: String, default: '' },
|
emoji: { type: String, default: '' },
|
||||||
value: { type: [String, Number], default: '' },
|
value: { type: [String, Number], default: '' },
|
||||||
|
compact: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -34,6 +29,12 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.conv-details--item {
|
.conv-details--item {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
padding: var(--space-slab) var(--space-normal);
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.conv-details--item__label {
|
.conv-details--item__label {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -46,19 +47,6 @@ export default {
|
||||||
|
|
||||||
.conv-details--item__value {
|
.conv-details--item__value {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
margin-left: var(--space-medium);
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title--icon .icon--emoji,
|
|
||||||
.title--icon .icon--font {
|
|
||||||
display: inline-block;
|
|
||||||
width: var(--space-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title--icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="multiselect-wrap--small">
|
<div class="multiselect-wrap--small">
|
||||||
<contact-details-item
|
<contact-details-item
|
||||||
|
compact
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
||||||
>
|
>
|
||||||
<template v-slot:button>
|
<template v-slot:button>
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-wrap--small">
|
<div class="multiselect-wrap--small">
|
||||||
<contact-details-item
|
<contact-details-item
|
||||||
|
compact
|
||||||
:title="$t('CONVERSATION_SIDEBAR.TEAM_LABEL')"
|
:title="$t('CONVERSATION_SIDEBAR.TEAM_LABEL')"
|
||||||
/>
|
/>
|
||||||
<multiselect-dropdown
|
<multiselect-dropdown
|
||||||
|
@ -64,68 +66,26 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<contact-details-item
|
<contact-details-item
|
||||||
|
compact
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_LABELS')"
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_LABELS')"
|
||||||
/>
|
/>
|
||||||
<conversation-labels :conversation-id="conversationId" />
|
<conversation-labels :conversation-id="conversationId" />
|
||||||
</div>
|
</div>
|
||||||
</accordion-item>
|
</accordion-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<accordion-item
|
<accordion-item
|
||||||
v-if="browser.browser_name"
|
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
|
||||||
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
|
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
|
||||||
|
compact
|
||||||
@click="value => toggleSidebarUIState('is_conv_details_open', value)"
|
@click="value => toggleSidebarUIState('is_conv_details_open', value)"
|
||||||
>
|
>
|
||||||
<div class="conversation--details">
|
<conversation-info
|
||||||
<contact-details-item
|
:conversation-attributes="conversationAdditionalAttributes"
|
||||||
v-if="location"
|
:contact-attributes="contactAdditionalAttributes"
|
||||||
:title="$t('CONTACT_FORM.FORM.LOCATION.LABEL')"
|
>
|
||||||
:value="location"
|
</conversation-info>
|
||||||
icon="ion-map"
|
|
||||||
emoji="📍"
|
|
||||||
/>
|
|
||||||
<contact-details-item
|
|
||||||
v-if="ipAddress"
|
|
||||||
:title="$t('CONTACT_PANEL.IP_ADDRESS')"
|
|
||||||
:value="ipAddress"
|
|
||||||
icon="ion-android-locate"
|
|
||||||
emoji="🧭"
|
|
||||||
/>
|
|
||||||
<contact-details-item
|
|
||||||
v-if="browser.browser_name"
|
|
||||||
:title="$t('CONTACT_PANEL.BROWSER')"
|
|
||||||
:value="browserName"
|
|
||||||
icon="ion-ios-world-outline"
|
|
||||||
emoji="🌐"
|
|
||||||
/>
|
|
||||||
<contact-details-item
|
|
||||||
v-if="browser.platform_name"
|
|
||||||
:title="$t('CONTACT_PANEL.OS')"
|
|
||||||
:value="platformName"
|
|
||||||
icon="ion-laptop"
|
|
||||||
emoji="💻"
|
|
||||||
/>
|
|
||||||
<contact-details-item
|
|
||||||
v-if="referer"
|
|
||||||
:title="$t('CONTACT_PANEL.INITIATED_FROM')"
|
|
||||||
:value="referer"
|
|
||||||
icon="ion-link"
|
|
||||||
emoji="🔗"
|
|
||||||
>
|
|
||||||
<a :href="referer" rel="noopener noreferrer nofollow" target="_blank">
|
|
||||||
{{ referer }}
|
|
||||||
</a>
|
|
||||||
</contact-details-item>
|
|
||||||
<contact-details-item
|
|
||||||
v-if="initiatedAt"
|
|
||||||
:title="$t('CONTACT_PANEL.INITIATED_AT')"
|
|
||||||
:value="initiatedAt.timestamp"
|
|
||||||
icon="ion-clock"
|
|
||||||
emoji="🕰"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</accordion-item>
|
</accordion-item>
|
||||||
|
|
||||||
<accordion-item
|
<accordion-item
|
||||||
v-if="hasContactAttributes"
|
v-if="hasContactAttributes"
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||||
|
@ -162,21 +122,21 @@ import ContactConversations from './ContactConversations.vue';
|
||||||
import ContactCustomAttributes from './ContactCustomAttributes';
|
import ContactCustomAttributes from './ContactCustomAttributes';
|
||||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
import ContactInfo from './contact/ContactInfo';
|
import ContactInfo from './contact/ContactInfo';
|
||||||
|
import ConversationInfo from './ConversationInfo';
|
||||||
import ConversationLabels from './labels/LabelBox.vue';
|
import ConversationLabels from './labels/LabelBox.vue';
|
||||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
import flag from 'country-code-emoji';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContactCustomAttributes,
|
AccordionItem,
|
||||||
ContactConversations,
|
ContactConversations,
|
||||||
|
ContactCustomAttributes,
|
||||||
ContactDetailsItem,
|
ContactDetailsItem,
|
||||||
ContactInfo,
|
ContactInfo,
|
||||||
|
ConversationInfo,
|
||||||
ConversationLabels,
|
ConversationLabels,
|
||||||
MultiselectDropdown,
|
MultiselectDropdown,
|
||||||
AccordionItem,
|
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, agentMixin, uiSettingsMixin],
|
mixins: [alertMixin, agentMixin, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
@ -200,68 +160,30 @@ export default {
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||||
}),
|
}),
|
||||||
|
conversationAdditionalAttributes() {
|
||||||
|
return this.currentConversationMetaData.additional_attributes || {};
|
||||||
|
},
|
||||||
|
channelType() {
|
||||||
|
return this.currentChat.meta?.channel;
|
||||||
|
},
|
||||||
|
contact() {
|
||||||
|
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||||
|
},
|
||||||
|
contactAdditionalAttributes() {
|
||||||
|
return this.contact.additional_attributes || {};
|
||||||
|
},
|
||||||
|
contactId() {
|
||||||
|
return this.currentChat.meta?.sender?.id;
|
||||||
|
},
|
||||||
currentConversationMetaData() {
|
currentConversationMetaData() {
|
||||||
return this.$store.getters[
|
return this.$store.getters[
|
||||||
'conversationMetadata/getConversationMetadata'
|
'conversationMetadata/getConversationMetadata'
|
||||||
](this.conversationId);
|
](this.conversationId);
|
||||||
},
|
},
|
||||||
additionalAttributes() {
|
|
||||||
return this.currentConversationMetaData.additional_attributes || {};
|
|
||||||
},
|
|
||||||
hasContactAttributes() {
|
hasContactAttributes() {
|
||||||
const { custom_attributes: customAttributes } = this.contact;
|
const { custom_attributes: customAttributes } = this.contact;
|
||||||
return customAttributes && Object.keys(customAttributes).length;
|
return customAttributes && Object.keys(customAttributes).length;
|
||||||
},
|
},
|
||||||
browser() {
|
|
||||||
return this.additionalAttributes.browser || {};
|
|
||||||
},
|
|
||||||
referer() {
|
|
||||||
return this.additionalAttributes.referer;
|
|
||||||
},
|
|
||||||
initiatedAt() {
|
|
||||||
return this.additionalAttributes.initiated_at;
|
|
||||||
},
|
|
||||||
browserName() {
|
|
||||||
return `${this.browser.browser_name || ''} ${this.browser
|
|
||||||
.browser_version || ''}`;
|
|
||||||
},
|
|
||||||
contactAdditionalAttributes() {
|
|
||||||
return this.contact.additional_attributes || {};
|
|
||||||
},
|
|
||||||
ipAddress() {
|
|
||||||
const { created_at_ip: createdAtIp } = this.contactAdditionalAttributes;
|
|
||||||
return createdAtIp;
|
|
||||||
},
|
|
||||||
location() {
|
|
||||||
const {
|
|
||||||
country = '',
|
|
||||||
city = '',
|
|
||||||
country_code: countryCode,
|
|
||||||
} = this.contactAdditionalAttributes;
|
|
||||||
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
|
||||||
|
|
||||||
if (!cityAndCountry) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const countryFlag = countryCode ? flag(countryCode) : '🌎';
|
|
||||||
return `${cityAndCountry} ${countryFlag}`;
|
|
||||||
},
|
|
||||||
platformName() {
|
|
||||||
const {
|
|
||||||
platform_name: platformName,
|
|
||||||
platform_version: platformVersion,
|
|
||||||
} = this.browser;
|
|
||||||
return `${platformName || ''} ${platformVersion || ''}`;
|
|
||||||
},
|
|
||||||
channelType() {
|
|
||||||
return this.currentChat.meta?.channel;
|
|
||||||
},
|
|
||||||
contactId() {
|
|
||||||
return this.currentChat.meta?.sender?.id;
|
|
||||||
},
|
|
||||||
contact() {
|
|
||||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
|
||||||
},
|
|
||||||
teamsList() {
|
teamsList() {
|
||||||
if (this.assignedTeam) {
|
if (this.assignedTeam) {
|
||||||
return [
|
return [
|
||||||
|
@ -330,6 +252,7 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getContactDetails();
|
this.getContactDetails();
|
||||||
|
this.$store.dispatch('attributes/get', 0);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onPanelToggle() {
|
onPanelToggle() {
|
||||||
|
@ -340,6 +263,11 @@ export default {
|
||||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getAttributesByModel() {
|
||||||
|
if (this.contactId) {
|
||||||
|
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||||
|
}
|
||||||
|
},
|
||||||
openTranscriptModal() {
|
openTranscriptModal() {
|
||||||
this.showTranscriptModal = true;
|
this.showTranscriptModal = true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<div class="conversation--details">
|
||||||
|
<contact-details-item
|
||||||
|
v-if="initiatedAt"
|
||||||
|
:title="$t('CONTACT_PANEL.INITIATED_AT')"
|
||||||
|
:value="initiatedAt.timestamp"
|
||||||
|
class="conversation--attribute"
|
||||||
|
/>
|
||||||
|
<contact-details-item
|
||||||
|
v-if="referer"
|
||||||
|
:title="$t('CONTACT_PANEL.INITIATED_FROM')"
|
||||||
|
:value="referer"
|
||||||
|
class="conversation--attribute"
|
||||||
|
>
|
||||||
|
<a :href="referer" rel="noopener noreferrer nofollow" target="_blank">
|
||||||
|
{{ referer }}
|
||||||
|
</a>
|
||||||
|
</contact-details-item>
|
||||||
|
<contact-details-item
|
||||||
|
v-if="browserName"
|
||||||
|
:title="$t('CONTACT_PANEL.BROWSER')"
|
||||||
|
:value="browserName"
|
||||||
|
class="conversation--attribute"
|
||||||
|
/>
|
||||||
|
<contact-details-item
|
||||||
|
v-if="platformName"
|
||||||
|
:title="$t('CONTACT_PANEL.OS')"
|
||||||
|
:value="platformName"
|
||||||
|
class="conversation--attribute"
|
||||||
|
/>
|
||||||
|
<contact-details-item
|
||||||
|
v-if="ipAddress"
|
||||||
|
:title="$t('CONTACT_PANEL.IP_ADDRESS')"
|
||||||
|
:value="ipAddress"
|
||||||
|
class="conversation--attribute"
|
||||||
|
/>
|
||||||
|
<custom-attributes
|
||||||
|
attribute-type="conversation_attribute"
|
||||||
|
attribute-class="conversation--attribute"
|
||||||
|
:class="customAttributeRowClass"
|
||||||
|
/>
|
||||||
|
<custom-attribute-selector attribute-type="conversation_attribute" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
|
import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||||
|
import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ContactDetailsItem,
|
||||||
|
CustomAttributes,
|
||||||
|
CustomAttributeSelector,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
conversationAttributes: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
contactAttributes: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
STATIC_ATTRIBUTES: [
|
||||||
|
{
|
||||||
|
name: 'initiated_at',
|
||||||
|
label: 'CONTACT_PANEL.INITIATED_AT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'referer',
|
||||||
|
label: 'CONTACT_PANEL.BROWSER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'browserName',
|
||||||
|
label: 'CONTACT_PANEL.BROWSER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'platformName',
|
||||||
|
label: 'CONTACT_PANEL.OS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ipAddress',
|
||||||
|
label: 'CONTACT_PANEL.IP_ADDRESS',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
referer() {
|
||||||
|
return this.conversationAttributes.referer;
|
||||||
|
},
|
||||||
|
initiatedAt() {
|
||||||
|
return this.conversationAttributes.initiated_at;
|
||||||
|
},
|
||||||
|
browserName() {
|
||||||
|
if (!this.conversationAttributes.browser) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
browser_name: browserName = '',
|
||||||
|
browser_version: browserVersion = '',
|
||||||
|
} = this.conversationAttributes.browser;
|
||||||
|
return `${browserName} ${browserVersion}`;
|
||||||
|
},
|
||||||
|
platformName() {
|
||||||
|
if (!this.conversationAttributes.browser) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
platform_name: platformName,
|
||||||
|
platform_version: platformVersion,
|
||||||
|
} = this.conversationAttributes.browser;
|
||||||
|
return `${platformName || ''} ${platformVersion || ''}`;
|
||||||
|
},
|
||||||
|
ipAddress() {
|
||||||
|
const { created_at_ip: createdAtIp } = this.contactAttributes;
|
||||||
|
return createdAtIp;
|
||||||
|
},
|
||||||
|
customAttributeRowClass() {
|
||||||
|
const attributes = [
|
||||||
|
'initiatedAt',
|
||||||
|
'referer',
|
||||||
|
'browserName',
|
||||||
|
'platformName',
|
||||||
|
'ipAddress',
|
||||||
|
];
|
||||||
|
const availableAttributes = attributes.filter(
|
||||||
|
attribute => !!this[attribute]
|
||||||
|
);
|
||||||
|
return availableAttributes.length % 2 === 0 ? 'even' : 'odd';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.conversation--attribute {
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
|
||||||
|
&:nth-child(2n) {
|
||||||
|
background: var(--b-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -41,19 +41,19 @@
|
||||||
emoji="📞"
|
emoji="📞"
|
||||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||||
/>
|
/>
|
||||||
<contact-info-row
|
|
||||||
v-if="additionalAttributes.location"
|
|
||||||
:value="additionalAttributes.location"
|
|
||||||
icon="ion-map"
|
|
||||||
emoji="🌍"
|
|
||||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
|
||||||
/>
|
|
||||||
<contact-info-row
|
<contact-info-row
|
||||||
:value="additionalAttributes.company_name"
|
:value="additionalAttributes.company_name"
|
||||||
icon="ion-briefcase"
|
icon="ion-briefcase"
|
||||||
emoji="🏢"
|
emoji="🏢"
|
||||||
:title="$t('CONTACT_PANEL.COMPANY')"
|
:title="$t('CONTACT_PANEL.COMPANY')"
|
||||||
/>
|
/>
|
||||||
|
<contact-info-row
|
||||||
|
v-if="location || additionalAttributes.location"
|
||||||
|
:value="location || additionalAttributes.location"
|
||||||
|
icon="ion-map"
|
||||||
|
emoji="🌍"
|
||||||
|
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-actions">
|
<div class="contact-actions">
|
||||||
|
@ -145,6 +145,7 @@ import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import adminMixin from '../../../../mixins/isAdmin';
|
import adminMixin from '../../../../mixins/isAdmin';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import flag from 'country-code-emoji';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -190,6 +191,20 @@ export default {
|
||||||
additionalAttributes() {
|
additionalAttributes() {
|
||||||
return this.contact.additional_attributes || {};
|
return this.contact.additional_attributes || {};
|
||||||
},
|
},
|
||||||
|
location() {
|
||||||
|
const {
|
||||||
|
country = '',
|
||||||
|
city = '',
|
||||||
|
country_code: countryCode,
|
||||||
|
} = this.additionalAttributes;
|
||||||
|
const cityAndCountry = [city, country].filter(item => !!item).join(', ');
|
||||||
|
|
||||||
|
if (!cityAndCountry) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const countryFlag = countryCode ? flag(countryCode) : '🌎';
|
||||||
|
return `${cityAndCountry} ${countryFlag}`;
|
||||||
|
},
|
||||||
socialProfiles() {
|
socialProfiles() {
|
||||||
const {
|
const {
|
||||||
social_profiles: socialProfiles,
|
social_profiles: socialProfiles,
|
||||||
|
@ -294,7 +309,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact--metadata {
|
.contact--metadata {
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-slab);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-actions {
|
.contact-actions {
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<div class="dropdown-search-wrap">
|
||||||
|
<h4 class="text-block-title">
|
||||||
|
{{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.TITLE') }}
|
||||||
|
</h4>
|
||||||
|
<div class="search-wrap">
|
||||||
|
<input
|
||||||
|
ref="searchbar"
|
||||||
|
v-model="search"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
autofocus="true"
|
||||||
|
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="list-wrap">
|
||||||
|
<div class="list">
|
||||||
|
<woot-dropdown-menu>
|
||||||
|
<custom-attribute-drop-down-item
|
||||||
|
v-for="attribute in filteredAttributes"
|
||||||
|
:key="attribute.attribute_display_name"
|
||||||
|
:title="attribute.attribute_display_name"
|
||||||
|
@click="onAddAttribute(attribute)"
|
||||||
|
/>
|
||||||
|
</woot-dropdown-menu>
|
||||||
|
<div v-if="noResult" class="no-result">
|
||||||
|
{{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.NO_RESULT') }}
|
||||||
|
</div>
|
||||||
|
<woot-button
|
||||||
|
variant="hollow"
|
||||||
|
class="add"
|
||||||
|
icon="ion-plus-round"
|
||||||
|
size="tiny"
|
||||||
|
@click="addNewAttribute"
|
||||||
|
>
|
||||||
|
{{ $t('CUSTOM_ATTRIBUTES.FORM.ADD.TITLE') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CustomAttributeDropDownItem from './CustomAttributeDropDownItem.vue';
|
||||||
|
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CustomAttributeDropDownItem,
|
||||||
|
},
|
||||||
|
mixins: [attributeMixin],
|
||||||
|
props: {
|
||||||
|
attributeType: {
|
||||||
|
type: String,
|
||||||
|
default: 'conversation_attribute',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filteredAttributes() {
|
||||||
|
return this.attributes
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
!Object.keys(this.customAttributes).includes(item.attribute_key)
|
||||||
|
)
|
||||||
|
.filter(attribute => {
|
||||||
|
return attribute.attribute_display_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(this.search.toLowerCase());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
noResult() {
|
||||||
|
return this.filteredAttributes.length === 0 && this.search !== '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.focusInput();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
focusInput() {
|
||||||
|
this.$refs.searchbar.focus();
|
||||||
|
},
|
||||||
|
addNewAttribute() {
|
||||||
|
this.$router.push(
|
||||||
|
`/app/accounts/${this.accountId}/settings/attributes/list`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async onAddAttribute(attribute) {
|
||||||
|
this.$emit('add-attribute', attribute);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dropdown-search-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 20rem;
|
||||||
|
|
||||||
|
.search-wrap {
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-height: var(--space-large);
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
height: var(--space-large);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
padding: var(--space-small);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border: 1px solid var(--w-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
width: 100%;
|
||||||
|
.add {
|
||||||
|
float: right;
|
||||||
|
margin-top: var(--space-one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--s-700);
|
||||||
|
padding: var(--space-smaller) var(--space-one);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button variant="clear" @click="onClick">
|
||||||
|
<span class="label-text" :title="title">{{ title }}</span>
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AttributeDropDownItem',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.$emit('click', this.title);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.item-wrap {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
::v-deep .button__content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
display: flex;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--w-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-label-wrap {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.label-color--display {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding-right: var(--space-small);
|
||||||
|
padding-left: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-color--display {
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
height: var(--space-slab);
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
margin-top: var(--space-micro);
|
||||||
|
min-width: var(--space-slab);
|
||||||
|
width: var(--space-slab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<div class="custom-attribute--selector">
|
||||||
|
<div
|
||||||
|
v-on-clickaway="closeDropdown"
|
||||||
|
class="label-wrap"
|
||||||
|
@keyup.esc="closeDropdown"
|
||||||
|
>
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
variant="link"
|
||||||
|
icon="ion-plus"
|
||||||
|
@click="toggleAttributeDropDown"
|
||||||
|
>
|
||||||
|
{{ $t('CUSTOM_ATTRIBUTES.ADD_BUTTON_TEXT') }}
|
||||||
|
</woot-button>
|
||||||
|
|
||||||
|
<div class="dropdown-wrap">
|
||||||
|
<div
|
||||||
|
:class="{ 'dropdown-pane--open': showAttributeDropDown }"
|
||||||
|
class="dropdown-pane"
|
||||||
|
>
|
||||||
|
<custom-attribute-drop-down
|
||||||
|
v-if="showAttributeDropDown"
|
||||||
|
:attribute-type="attributeType"
|
||||||
|
@add-attribute="addAttribute"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CustomAttributeDropDown from './CustomAttributeDropDown.vue';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CustomAttributeDropDown,
|
||||||
|
},
|
||||||
|
mixins: [clickaway, alertMixin, attributeMixin],
|
||||||
|
props: {
|
||||||
|
attributeType: {
|
||||||
|
type: String,
|
||||||
|
default: 'conversation_attribute',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAttributeDropDown: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async addAttribute(attribute) {
|
||||||
|
const { attribute_key } = attribute;
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('updateCustomAttributes', {
|
||||||
|
conversationId: this.conversationId,
|
||||||
|
customAttributes: {
|
||||||
|
...this.customAttributes,
|
||||||
|
[attribute_key]: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
bus.$emit(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, attribute_key);
|
||||||
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.ADD.SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.message ||
|
||||||
|
this.$t('CUSTOM_ATTRIBUTES.FORM.ADD.ERROR');
|
||||||
|
this.showAlert(errorMessage);
|
||||||
|
} finally {
|
||||||
|
this.closeDropdown();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAttributeDropDown() {
|
||||||
|
this.showAttributeDropDown = !this.showAttributeDropDown;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDropdown() {
|
||||||
|
this.showAttributeDropDown = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.custom-attribute--selector {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-slab) var(--space-normal);
|
||||||
|
|
||||||
|
.label-wrap {
|
||||||
|
line-height: var(--space-medium);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.dropdown-wrap {
|
||||||
|
display: flex;
|
||||||
|
left: -1px;
|
||||||
|
margin-right: var(--space-medium);
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-medium);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.dropdown-pane {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--r-500);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class="custom-attributes--panel">
|
||||||
|
<custom-attribute
|
||||||
|
v-for="attribute in filteredAttributes"
|
||||||
|
:key="attribute.id"
|
||||||
|
:attribute-key="attribute.attribute_key"
|
||||||
|
:attribute-type="attribute.attribute_display_type"
|
||||||
|
:label="attribute.attribute_display_name"
|
||||||
|
:icon="attribute.icon"
|
||||||
|
emoji=""
|
||||||
|
:value="attribute.value"
|
||||||
|
:show-actions="true"
|
||||||
|
:class="attributeClass"
|
||||||
|
@update="onUpdate"
|
||||||
|
@delete="onDelete"
|
||||||
|
@copy="onCopy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import copy from 'copy-text-to-clipboard';
|
||||||
|
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CustomAttribute,
|
||||||
|
},
|
||||||
|
mixins: [alertMixin, attributeMixin],
|
||||||
|
props: {
|
||||||
|
attributeType: {
|
||||||
|
type: String,
|
||||||
|
default: 'conversation_attribute',
|
||||||
|
},
|
||||||
|
attributeClass: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onUpdate(key, value) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('updateCustomAttributes', {
|
||||||
|
conversationId: this.conversationId,
|
||||||
|
customAttributes: { ...this.customAttributes, [key]: value },
|
||||||
|
});
|
||||||
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.message ||
|
||||||
|
this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
|
||||||
|
this.showAlert(errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onDelete(key) {
|
||||||
|
const { [key]: remove, ...updatedAttributes } = this.customAttributes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('updateCustomAttributes', {
|
||||||
|
conversationId: this.conversationId,
|
||||||
|
customAttributes: updatedAttributes,
|
||||||
|
});
|
||||||
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.message ||
|
||||||
|
this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
|
||||||
|
this.showAlert(errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCopy(attributeValue) {
|
||||||
|
copy(attributeValue);
|
||||||
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.custom-attributes--panel {
|
||||||
|
.conversation--attribute {
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.odd {
|
||||||
|
.conversation--attribute {
|
||||||
|
&:nth-child(2n + 1) {
|
||||||
|
background: var(--b-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.even {
|
||||||
|
.conversation--attribute {
|
||||||
|
&:nth-child(2n) {
|
||||||
|
background: var(--b-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,8 +3,19 @@
|
||||||
<div class="column content-box">
|
<div class="column content-box">
|
||||||
<woot-modal-header :header-title="$t('ATTRIBUTES_MGMT.ADD.TITLE')" />
|
<woot-modal-header :header-title="$t('ATTRIBUTES_MGMT.ADD.TITLE')" />
|
||||||
|
|
||||||
<form class="row" @submit.prevent="addAttributes()">
|
<form class="row" @submit.prevent="addAttributes">
|
||||||
<div class="medium-12 columns">
|
<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>
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model="displayName"
|
v-model="displayName"
|
||||||
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL')"
|
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.LABEL')"
|
||||||
|
@ -22,7 +33,7 @@
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.LABEL') }}
|
||||||
<textarea
|
<textarea
|
||||||
v-model="description"
|
v-model="description"
|
||||||
rows="5"
|
rows="3"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
|
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.DESC.PLACEHOLDER')"
|
||||||
@blur="$v.description.$touch"
|
@blur="$v.description.$touch"
|
||||||
|
@ -31,18 +42,6 @@
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.DESC.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
|
||||||
|
|
||||||
<label :class="{ error: $v.attributeType.$error }">
|
<label :class="{ error: $v.attributeType.$error }">
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.LABEL') }}
|
||||||
<select v-model="attributeType">
|
<select v-model="attributeType">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
/>
|
/>
|
||||||
</woot-tabs>
|
</woot-tabs>
|
||||||
|
|
||||||
<div class="columns with-right-space ">
|
<div class="columns with-right-space">
|
||||||
<p
|
<p
|
||||||
v-if="!uiFlags.isFetching && !attributes.length"
|
v-if="!uiFlags.isFetching && !attributes.length"
|
||||||
class="no-items-error-message"
|
class="no-items-error-message"
|
||||||
|
@ -135,10 +135,6 @@ export default {
|
||||||
key: 0,
|
key: 0,
|
||||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
name: this.$t('ATTRIBUTES_MGMT.TABS.CONVERSATION'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 1,
|
|
||||||
name: this.$t('ATTRIBUTES_MGMT.TABS.CONTACT'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
deleteConfirmText() {
|
deleteConfirmText() {
|
||||||
|
|
|
@ -3,10 +3,6 @@ export const ATTRIBUTE_MODELS = [
|
||||||
id: 0,
|
id: 0,
|
||||||
option: 'Conversation',
|
option: 'Conversation',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
option: 'Contact',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ATTRIBUTE_TYPES = [
|
export const ATTRIBUTE_TYPES = [
|
||||||
|
@ -18,14 +14,6 @@ export const ATTRIBUTE_TYPES = [
|
||||||
id: 1,
|
id: 1,
|
||||||
option: 'Number',
|
option: 'Number',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
option: 'Currency',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
option: 'Percent',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
option: 'Link',
|
option: 'Link',
|
||||||
|
|
|
@ -10,6 +10,7 @@ import profile from './profile/profile.routes';
|
||||||
import reports from './reports/reports.routes';
|
import reports from './reports/reports.routes';
|
||||||
import campaigns from './campaigns/campaigns.routes';
|
import campaigns from './campaigns/campaigns.routes';
|
||||||
import teams from './teams/teams.routes';
|
import teams from './teams/teams.routes';
|
||||||
|
import attributes from './attributes/attributes.routes';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -36,5 +37,6 @@ export default {
|
||||||
...teams.routes,
|
...teams.routes,
|
||||||
...campaigns.routes,
|
...campaigns.routes,
|
||||||
...integrationapps.routes,
|
...integrationapps.routes,
|
||||||
|
...attributes.routes,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -278,6 +278,25 @@ const actions = {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateCustomAttributes: async (
|
||||||
|
{ commit },
|
||||||
|
{ conversationId, customAttributes }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await ConversationApi.updateCustomAttributes({
|
||||||
|
conversationId,
|
||||||
|
customAttributes,
|
||||||
|
});
|
||||||
|
const { custom_attributes } = response.data;
|
||||||
|
commit(
|
||||||
|
types.default.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES,
|
||||||
|
custom_attributes
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default actions;
|
export default actions;
|
||||||
|
|
|
@ -69,6 +69,14 @@ export const mutations = {
|
||||||
Vue.set(chat.meta, 'team', team);
|
Vue.set(chat.meta, 'team', team);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[types.default.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](
|
||||||
|
_state,
|
||||||
|
custom_attributes
|
||||||
|
) {
|
||||||
|
const [chat] = getSelectedChatConversation(_state);
|
||||||
|
Vue.set(chat, 'custom_attributes', custom_attributes);
|
||||||
|
},
|
||||||
|
|
||||||
[types.default.CHANGE_CONVERSATION_STATUS](
|
[types.default.CHANGE_CONVERSATION_STATUS](
|
||||||
_state,
|
_state,
|
||||||
{ conversationId, status, snoozedUntil }
|
{ conversationId, status, snoozedUntil }
|
||||||
|
|
|
@ -281,4 +281,25 @@ describe('#deleteMessage', () => {
|
||||||
).rejects.toThrow(Error);
|
).rejects.toThrow(Error);
|
||||||
expect(commit.mock.calls).toEqual([]);
|
expect(commit.mock.calls).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#updateCustomAttributes', () => {
|
||||||
|
it('update conversation custom attributes', async () => {
|
||||||
|
axios.post.mockResolvedValue({
|
||||||
|
data: { custom_attributes: { order_d: '1001' } },
|
||||||
|
});
|
||||||
|
await actions.updateCustomAttributes(
|
||||||
|
{ commit },
|
||||||
|
{
|
||||||
|
conversationId: 1,
|
||||||
|
customAttributes: { order_d: '1001' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
types.default.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES,
|
||||||
|
{ order_d: '1001' },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -186,5 +186,19 @@ describe('#mutations', () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES', () => {
|
||||||
|
it('update conversation custom attributes', () => {
|
||||||
|
const custom_attributes = { order_id: 1001 };
|
||||||
|
const state = { allConversations: [{ id: 1 }], selectedChatId: 1 };
|
||||||
|
mutations[types.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](state, {
|
||||||
|
conversationId: 1,
|
||||||
|
custom_attributes,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
state.allConversations[0].custom_attributes.custom_attributes
|
||||||
|
).toEqual(custom_attributes);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,6 +37,8 @@ export default {
|
||||||
MARK_MESSAGE_READ: 'MARK_MESSAGE_READ',
|
MARK_MESSAGE_READ: 'MARK_MESSAGE_READ',
|
||||||
SET_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
|
SET_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
|
||||||
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
|
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
|
||||||
|
UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES:
|
||||||
|
'UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES',
|
||||||
|
|
||||||
SET_CONVERSATION_CAN_REPLY: 'SET_CONVERSATION_CAN_REPLY',
|
SET_CONVERSATION_CAN_REPLY: 'SET_CONVERSATION_CAN_REPLY',
|
||||||
|
|
||||||
|
|
|
@ -3,4 +3,5 @@ export const BUS_EVENTS = {
|
||||||
SET_TWEET_REPLY: 'SET_TWEET_REPLY',
|
SET_TWEET_REPLY: 'SET_TWEET_REPLY',
|
||||||
SHOW_ALERT: 'SHOW_ALERT',
|
SHOW_ALERT: 'SHOW_ALERT',
|
||||||
START_NEW_CONVERSATION: 'START_NEW_CONVERSATION',
|
START_NEW_CONVERSATION: 'START_NEW_CONVERSATION',
|
||||||
|
FOCUS_CUSTOM_ATTRIBUTE: 'FOCUS_CUSTOM_ATTRIBUTE',
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue