mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 10:12:34 +00:00
feat: Render contact custom attributes in contact/conversation sidebar (#3310)
This commit is contained in:
@@ -14,6 +14,7 @@ Metrics/ClassLength:
|
|||||||
- 'app/mailers/conversation_reply_mailer.rb'
|
- 'app/mailers/conversation_reply_mailer.rb'
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/builders/messages/facebook/message_builder.rb'
|
||||||
|
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index, :active, :search]
|
before_action :set_current_page, only: [:index, :active, :search]
|
||||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
|
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes, :destroy_custom_attributes]
|
||||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@@ -64,6 +64,12 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@contactable_inboxes = @all_contactable_inboxes.select { |contactable_inbox| policy(contactable_inbox[:inbox]).show? }
|
@contactable_inboxes = @all_contactable_inboxes.select { |contactable_inbox| policy(contactable_inbox[:inbox]).show? }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO : refactor this method into dedicated contacts/custom_attributes controller class and routes
|
||||||
|
def destroy_custom_attributes
|
||||||
|
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
|
||||||
|
@contact.save!
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@contact = Current.account.contacts.new(contact_params)
|
@contact = Current.account.contacts.new(contact_params)
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
|
|||||||
private
|
private
|
||||||
|
|
||||||
def fetch_custom_attributes_definitions
|
def fetch_custom_attributes_definitions
|
||||||
@custom_attribute_definitions = Current.account.custom_attribute_definitions.where(
|
@custom_attribute_definitions = Current.account.custom_attribute_definitions.with_attribute_model(permitted_params[:attribute_model])
|
||||||
attribute_model: permitted_params[:attribute_model] || DEFAULT_ATTRIBUTE_MODEL
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_custom_attribute_definition
|
def fetch_custom_attribute_definition
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ class AttributeAPI extends ApiClient {
|
|||||||
super('custom_attribute_definitions', { accountScoped: true });
|
super('custom_attribute_definitions', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAttributesByModel(modelId) {
|
getAttributesByModel() {
|
||||||
return axios.get(`${this.url}?attribute_model=${modelId}`);
|
return axios.get(this.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ class ContactAPI extends ApiClient {
|
|||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroyCustomAttributes(contactId, customAttributes) {
|
||||||
|
return axios.post(`${this.url}/${contactId}/destroy_custom_attributes`, {
|
||||||
|
custom_attributes: customAttributes,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ContactAPI();
|
export default new ContactAPI();
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ describe('#ContactsAPI', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#destroyCustomAttributes', () => {
|
||||||
|
contactAPI.destroyCustomAttributes(1, ['cloudCustomer']);
|
||||||
|
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/contacts/1/destroy_custom_attributes',
|
||||||
|
{
|
||||||
|
custom_attributes: ['cloudCustomer'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('#importContacts', () => {
|
it('#importContacts', () => {
|
||||||
const file = 'file';
|
const file = 'file';
|
||||||
contactAPI.importContacts(file);
|
contactAPI.importContacts(file);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contact-attribute">
|
<div class="custom-attribute">
|
||||||
<div class="title-wrap">
|
<div class="title-wrap">
|
||||||
<h4 class="text-block-title title error">
|
<h4 class="text-block-title title error">
|
||||||
<span class="attribute-name" :class="{ error: $v.editedValue.$error }">
|
<span class="attribute-name" :class="{ error: $v.editedValue.$error }">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
{{ value || '---' }}
|
{{ value || '---' }}
|
||||||
</a>
|
</a>
|
||||||
<p v-else class="value">
|
<p v-else class="value">
|
||||||
{{ value || '---' }}
|
{{ formattedValue || '---' }}
|
||||||
</p>
|
</p>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showActions"
|
v-if="showActions"
|
||||||
@@ -79,9 +79,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import format from 'date-fns/format';
|
||||||
import { required, url } from 'vuelidate/lib/validators';
|
import { required, url } from 'vuelidate/lib/validators';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
|
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
label: { type: String, required: true },
|
label: { type: String, required: true },
|
||||||
@@ -89,11 +92,15 @@ export default {
|
|||||||
showActions: { type: Boolean, default: false },
|
showActions: { type: Boolean, default: false },
|
||||||
attributeType: { type: String, default: 'text' },
|
attributeType: { type: String, default: 'text' },
|
||||||
attributeKey: { type: String, required: true },
|
attributeKey: { type: String, required: true },
|
||||||
|
contactId: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
editedValue: this.value,
|
editedValue:
|
||||||
|
this.attributeType === 'date'
|
||||||
|
? format(new Date(this.value), DATE_FORMAT)
|
||||||
|
: this.value,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations() {
|
validations() {
|
||||||
@@ -123,6 +130,12 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||||
},
|
},
|
||||||
|
formattedValue() {
|
||||||
|
if (this.attributeType === 'date') {
|
||||||
|
return format(new Date(this.editedValue), 'dd-MM-yyyy');
|
||||||
|
}
|
||||||
|
return this.editedValue;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
bus.$on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, focusAttributeKey => {
|
||||||
@@ -144,12 +157,17 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
|
const updatedValue =
|
||||||
|
this.attributeType === 'date'
|
||||||
|
? format(new Date(this.editedValue), DATE_FORMAT)
|
||||||
|
: this.editedValue;
|
||||||
|
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) {
|
if (this.$v.$invalid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
this.$emit('update', this.attributeKey, this.editedValue);
|
this.$emit('update', this.attributeKey, updatedValue);
|
||||||
},
|
},
|
||||||
onDelete() {
|
onDelete() {
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
@@ -163,7 +181,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.contact-attribute {
|
.custom-attribute {
|
||||||
padding: var(--space-slab) var(--space-normal);
|
padding: var(--space-slab) var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export default {
|
|||||||
this.$store.dispatch('inboxes/get');
|
this.$store.dispatch('inboxes/get');
|
||||||
this.$store.dispatch('notifications/unReadCount');
|
this.$store.dispatch('notifications/unReadCount');
|
||||||
this.$store.dispatch('teams/get');
|
this.$store.dispatch('teams/get');
|
||||||
|
this.$store.dispatch('attributes/get');
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"ATTRIBUTES_MGMT": {
|
"ATTRIBUTES_MGMT": {
|
||||||
"HEADER": "Attributes",
|
"HEADER": "Custom Attributes",
|
||||||
"HEADER_BTN_TXT": "Add Attribute",
|
"HEADER_BTN_TXT": "Add Custom Attribute",
|
||||||
"LOADING": "Fetching attributes",
|
"LOADING": "Fetching custom 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>",
|
"SIDEBAR_TXT": "<p><b>Custom 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 Custom Attribute, just click on the <b>Add Custom Attribute.</b> You can also edit or delete an existing Custom Attribute by clicking on the Edit or Delete button.</p>",
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Add attribute",
|
"TITLE": "Add Custom Attribute",
|
||||||
"SUBMIT": "Create",
|
"SUBMIT": "Create",
|
||||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Display Name",
|
"LABEL": "Display Name",
|
||||||
"PLACEHOLDER": "Enter attribute display name",
|
"PLACEHOLDER": "Enter custom attribute display name",
|
||||||
"ERROR": "Name is required"
|
"ERROR": "Name is required"
|
||||||
},
|
},
|
||||||
"DESC": {
|
"DESC": {
|
||||||
"LABEL": "Description",
|
"LABEL": "Description",
|
||||||
"PLACEHOLDER": "Enter attribute description",
|
"PLACEHOLDER": "Enter custom attribute description",
|
||||||
"ERROR": "Description is required"
|
"ERROR": "Description is required"
|
||||||
},
|
},
|
||||||
"MODEL": {
|
"MODEL": {
|
||||||
@@ -30,34 +30,36 @@
|
|||||||
"ERROR": "Type is required"
|
"ERROR": "Type is required"
|
||||||
},
|
},
|
||||||
"KEY": {
|
"KEY": {
|
||||||
"LABEL": "Key"
|
"LABEL": "Key",
|
||||||
|
"PLACEHOLDER": "Enter custom attribute key",
|
||||||
|
"ERROR": "Key is required"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Attribute added successfully",
|
"SUCCESS_MESSAGE": "Custom Attribute added successfully",
|
||||||
"ERROR_MESSAGE": "Could not able to create an attribute, Please try again later"
|
"ERROR_MESSAGE": "Could not able to create a custom attribute, Please try again later"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"BUTTON_TEXT": "Delete",
|
"BUTTON_TEXT": "Delete",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Attribute deleted successfully.",
|
"SUCCESS_MESSAGE": "Custom Attribute deleted successfully.",
|
||||||
"ERROR_MESSAGE": "Couldn't delete the attribute. Try again."
|
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
|
||||||
},
|
},
|
||||||
"CONFIRM": {
|
"CONFIRM": {
|
||||||
"TITLE": "Are you sure want to delete - %{attributeName}",
|
"TITLE": "Are you sure want to delete - %{attributeName}",
|
||||||
"PLACE_HOLDER": "Please type {attributeName} to confirm",
|
"PLACE_HOLDER": "Please type {attributeName} to confirm",
|
||||||
"MESSAGE": "Deleting will remove the attribute",
|
"MESSAGE": "Deleting will remove the custom attribute",
|
||||||
"YES": "Delete ",
|
"YES": "Delete ",
|
||||||
"NO": "Cancel"
|
"NO": "Cancel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT": {
|
"EDIT": {
|
||||||
"TITLE": "Edit attribute",
|
"TITLE": "Edit Custom Attribute",
|
||||||
"UPDATE_BUTTON_TEXT": "Update",
|
"UPDATE_BUTTON_TEXT": "Update",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Attribute updated successfully",
|
"SUCCESS_MESSAGE": "Custom Attribute updated successfully",
|
||||||
"ERROR_MESSAGE": "There was an error updating attribute, please try again"
|
"ERROR_MESSAGE": "There was an error updating custom attribute, please try again"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TABS": {
|
"TABS": {
|
||||||
@@ -72,8 +74,8 @@
|
|||||||
"DELETE": "Delete"
|
"DELETE": "Delete"
|
||||||
},
|
},
|
||||||
"EMPTY_RESULT": {
|
"EMPTY_RESULT": {
|
||||||
"404": "There are no attributes created",
|
"404": "There are no custom attributes created",
|
||||||
"NOT_FOUND": "There are no attributes configured"
|
"NOT_FOUND": "There are no custom attributes configured"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@
|
|||||||
"PLACEHOLDER": "Eg: 11901 "
|
"PLACEHOLDER": "Eg: 11901 "
|
||||||
},
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Add",
|
"TITLE": "Create new attribute ",
|
||||||
"SUCCESS": "Attribute added successfully",
|
"SUCCESS": "Attribute added successfully",
|
||||||
"ERROR": "Unable to add attribute. Please try again later"
|
"ERROR": "Unable to add attribute. Please try again later"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
"ACCOUNT_SETTINGS": "Account Settings",
|
"ACCOUNT_SETTINGS": "Account Settings",
|
||||||
"APPLICATIONS": "Applications",
|
"APPLICATIONS": "Applications",
|
||||||
"LABELS": "Labels",
|
"LABELS": "Labels",
|
||||||
"ATTRIBUTES": "Attributes",
|
"CUSTOM_ATTRIBUTES": "Custom Attributes",
|
||||||
"TEAMS": "Teams",
|
"TEAMS": "Teams",
|
||||||
"ALL_CONTACTS": "All Contacts",
|
"ALL_CONTACTS": "All Contacts",
|
||||||
"TAGGED_WITH": "Tagged with",
|
"TAGGED_WITH": "Tagged with",
|
||||||
|
|||||||
@@ -67,9 +67,11 @@ const settings = accountId => ({
|
|||||||
},
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
icon: 'ion-code',
|
icon: 'ion-code',
|
||||||
label: 'ATTRIBUTES',
|
label: 'CUSTOM_ATTRIBUTES',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/attributes/list`),
|
toState: frontendURL(
|
||||||
|
`accounts/${accountId}/settings/custom-attributes/list`
|
||||||
|
),
|
||||||
toStateName: 'attributes_list',
|
toStateName: 'attributes_list',
|
||||||
},
|
},
|
||||||
cannedResponses: {
|
cannedResponses: {
|
||||||
|
|||||||
@@ -8,31 +8,50 @@ export default {
|
|||||||
}),
|
}),
|
||||||
attributes() {
|
attributes() {
|
||||||
return this.$store.getters['attributes/getAttributesByModel'](
|
return this.$store.getters['attributes/getAttributesByModel'](
|
||||||
'conversation_attribute'
|
this.attributeType
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
customAttributes() {
|
customAttributes() {
|
||||||
return this.currentChat.custom_attributes || {};
|
if (this.attributeType === 'conversation_attribute')
|
||||||
|
return this.currentChat.custom_attributes || {};
|
||||||
|
return this.contact.custom_attributes || {};
|
||||||
|
},
|
||||||
|
contactIdentifier() {
|
||||||
|
return (
|
||||||
|
this.currentChat.meta?.sender?.id ||
|
||||||
|
this.$route.params.contactId ||
|
||||||
|
this.contactId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
contact() {
|
||||||
|
return this.$store.getters['contacts/getContact'](this.contactIdentifier);
|
||||||
},
|
},
|
||||||
conversationId() {
|
conversationId() {
|
||||||
return this.currentChat.id;
|
return this.currentChat.id;
|
||||||
},
|
},
|
||||||
// Select only custom attribute which are already defined
|
|
||||||
filteredAttributes() {
|
filteredAttributes() {
|
||||||
return Object.keys(this.customAttributes)
|
return Object.keys(this.customAttributes).map(key => {
|
||||||
.filter(key => {
|
const item = this.attributes.find(
|
||||||
return this.attributes.find(item => item.attribute_key === key);
|
attribute => attribute.attribute_key === key
|
||||||
})
|
);
|
||||||
.map(key => {
|
if (item) {
|
||||||
const item = this.attributes.find(
|
|
||||||
attribute => attribute.attribute_key === key
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
value: this.customAttributes[key],
|
value: this.customAttributes[key],
|
||||||
icon: this.attributeIcon(item.attribute_display_type),
|
icon: this.attributeIcon(item.attribute_display_type),
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: this.customAttributes[key],
|
||||||
|
attribute_description: key,
|
||||||
|
attribute_display_name: key,
|
||||||
|
attribute_display_type: 'text',
|
||||||
|
attribute_key: key,
|
||||||
|
attribute_model: this.attributeType,
|
||||||
|
id: Math.random(),
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -19,24 +19,18 @@ describe('attributeMixin', () => {
|
|||||||
custom_attributes: {
|
custom_attributes: {
|
||||||
product_id: 2021,
|
product_id: 2021,
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
sender: {
|
||||||
|
id: 1212,
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
getCurrentAccountId: () => 1,
|
getCurrentAccountId: () => 1,
|
||||||
};
|
attributeType: () => 'conversation_attribute',
|
||||||
|
};
|
||||||
store = new Vuex.Store({ actions, getters });
|
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', () => {
|
it('returns currently selected conversation id', () => {
|
||||||
const Component = {
|
const Component = {
|
||||||
render() {},
|
render() {},
|
||||||
@@ -56,6 +50,14 @@ describe('attributeMixin', () => {
|
|||||||
attributes() {
|
attributes() {
|
||||||
return attributeFixtures;
|
return attributeFixtures;
|
||||||
},
|
},
|
||||||
|
contact() {
|
||||||
|
return {
|
||||||
|
id: 7165,
|
||||||
|
custom_attributes: {
|
||||||
|
product_id: 2021,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
@@ -86,4 +88,83 @@ describe('attributeMixin', () => {
|
|||||||
expect(wrapper.vm.attributeIcon('date')).toBe('ion-calendar');
|
expect(wrapper.vm.attributeIcon('date')).toBe('ion-calendar');
|
||||||
expect(wrapper.vm.attributeIcon()).toBe('ion-edit');
|
expect(wrapper.vm.attributeIcon()).toBe('ion-edit');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns currently selected contact', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [attributeMixin],
|
||||||
|
computed: {
|
||||||
|
contact() {
|
||||||
|
return {
|
||||||
|
id: 7165,
|
||||||
|
custom_attributes: {
|
||||||
|
product_id: 2021,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.contact).toEqual({
|
||||||
|
id: 7165,
|
||||||
|
custom_attributes: {
|
||||||
|
product_id: 2021,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns currently selected contact id', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [attributeMixin],
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.contactIdentifier).toEqual(1212);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns currently selected conversation custom attributes', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [attributeMixin],
|
||||||
|
computed: {
|
||||||
|
contact() {
|
||||||
|
return {
|
||||||
|
id: 7165,
|
||||||
|
custom_attributes: {
|
||||||
|
product_id: 2021,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.customAttributes).toEqual({
|
||||||
|
product_id: 2021,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns currently selected contact custom attributes', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [attributeMixin],
|
||||||
|
computed: {
|
||||||
|
contact() {
|
||||||
|
return {
|
||||||
|
id: 7165,
|
||||||
|
custom_attributes: {
|
||||||
|
cloudCustomer: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.customAttributes).toEqual({
|
||||||
|
cloudCustomer: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,12 +13,21 @@
|
|||||||
@panel-close="onClose"
|
@panel-close="onClose"
|
||||||
/>
|
/>
|
||||||
<accordion-item
|
<accordion-item
|
||||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||||
|
compact
|
||||||
@click="value => toggleSidebarUIState('is_ct_custom_attr_open', value)"
|
@click="value => toggleSidebarUIState('is_ct_custom_attr_open', value)"
|
||||||
>
|
>
|
||||||
<contact-custom-attributes
|
<custom-attributes
|
||||||
|
:contact-id="contact.id"
|
||||||
|
attribute-type="contact_attribute"
|
||||||
|
attribute-class="conversation--attribute"
|
||||||
:custom-attributes="contact.custom_attributes"
|
:custom-attributes="contact.custom_attributes"
|
||||||
|
class="even"
|
||||||
|
/>
|
||||||
|
<custom-attribute-selector
|
||||||
|
attribute-type="contact_attribute"
|
||||||
|
:contact-id="contact.id"
|
||||||
/>
|
/>
|
||||||
</accordion-item>
|
</accordion-item>
|
||||||
<accordion-item
|
<accordion-item
|
||||||
@@ -45,9 +54,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem';
|
import AccordionItem from 'dashboard/components/Accordion/AccordionItem';
|
||||||
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations';
|
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations';
|
||||||
import ContactCustomAttributes from 'dashboard/routes/dashboard/conversation/ContactCustomAttributes';
|
|
||||||
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo';
|
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo';
|
||||||
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
||||||
|
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
|
||||||
|
import CustomAttributeSelector from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributeSelector.vue';
|
||||||
|
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
@@ -55,9 +65,10 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
ContactConversations,
|
ContactConversations,
|
||||||
ContactCustomAttributes,
|
|
||||||
ContactInfo,
|
ContactInfo,
|
||||||
ContactLabel,
|
ContactLabel,
|
||||||
|
CustomAttributes,
|
||||||
|
CustomAttributeSelector,
|
||||||
},
|
},
|
||||||
mixins: [uiSettingsMixin],
|
mixins: [uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -85,19 +85,26 @@
|
|||||||
>
|
>
|
||||||
</conversation-info>
|
</conversation-info>
|
||||||
</accordion-item>
|
</accordion-item>
|
||||||
|
|
||||||
<accordion-item
|
<accordion-item
|
||||||
v-if="hasContactAttributes"
|
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||||
:is-open="isContactSidebarItemOpen('is_contact_attributes_open')"
|
:is-open="isContactSidebarItemOpen('is_contact_attributes_open')"
|
||||||
|
compact
|
||||||
@click="
|
@click="
|
||||||
value => toggleSidebarUIState('is_contact_attributes_open', value)
|
value => toggleSidebarUIState('is_contact_attributes_open', value)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<contact-custom-attributes
|
<custom-attributes
|
||||||
:custom-attributes="contact.custom_attributes"
|
attribute-type="contact_attribute"
|
||||||
|
attribute-class="conversation--attribute"
|
||||||
|
class="even"
|
||||||
|
:contact-id="contact.id"
|
||||||
|
/>
|
||||||
|
<custom-attribute-selector
|
||||||
|
attribute-type="contact_attribute"
|
||||||
|
:contact-id="contact.id"
|
||||||
/>
|
/>
|
||||||
</accordion-item>
|
</accordion-item>
|
||||||
|
|
||||||
<accordion-item
|
<accordion-item
|
||||||
v-if="contact.id"
|
v-if="contact.id"
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.PREVIOUS_CONVERSATION')"
|
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.PREVIOUS_CONVERSATION')"
|
||||||
@@ -119,24 +126,27 @@ import agentMixin from '../../../mixins/agentMixin';
|
|||||||
|
|
||||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem';
|
import AccordionItem from 'dashboard/components/Accordion/AccordionItem';
|
||||||
import ContactConversations from './ContactConversations.vue';
|
import ContactConversations from './ContactConversations.vue';
|
||||||
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 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 CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||||
|
import CustomAttributeSelector from './customAttributes/CustomAttributeSelector.vue';
|
||||||
|
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
ContactConversations,
|
ContactConversations,
|
||||||
ContactCustomAttributes,
|
|
||||||
ContactDetailsItem,
|
ContactDetailsItem,
|
||||||
ContactInfo,
|
ContactInfo,
|
||||||
ConversationInfo,
|
ConversationInfo,
|
||||||
ConversationLabels,
|
ConversationLabels,
|
||||||
MultiselectDropdown,
|
MultiselectDropdown,
|
||||||
|
CustomAttributes,
|
||||||
|
CustomAttributeSelector,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, agentMixin, uiSettingsMixin],
|
mixins: [alertMixin, agentMixin, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.NO_RESULT') }}
|
{{ $t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_SELECT.NO_RESULT') }}
|
||||||
</div>
|
</div>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="hollow"
|
|
||||||
class="add"
|
class="add"
|
||||||
icon="ion-plus-round"
|
icon="ion-plus-round"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@@ -53,6 +52,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'conversation_attribute',
|
default: 'conversation_attribute',
|
||||||
},
|
},
|
||||||
|
contactId: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -76,7 +76,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
noResult() {
|
noResult() {
|
||||||
return this.filteredAttributes.length === 0 && this.search !== '';
|
return this.filteredAttributes.length === 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export default {
|
|||||||
},
|
},
|
||||||
addNewAttribute() {
|
addNewAttribute() {
|
||||||
this.$router.push(
|
this.$router.push(
|
||||||
`/app/accounts/${this.accountId}/settings/attributes/list`
|
`/app/accounts/${this.accountId}/settings/custom-attributes/list`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async onAddAttribute(attribute) {
|
async onAddAttribute(attribute) {
|
||||||
@@ -138,7 +138,6 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
.add {
|
.add {
|
||||||
float: right;
|
float: right;
|
||||||
margin-top: var(--space-one);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<custom-attribute-drop-down
|
<custom-attribute-drop-down
|
||||||
v-if="showAttributeDropDown"
|
v-if="showAttributeDropDown"
|
||||||
:attribute-type="attributeType"
|
:attribute-type="attributeType"
|
||||||
|
:contact-id="contactId"
|
||||||
@add-attribute="addAttribute"
|
@add-attribute="addAttribute"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +48,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'conversation_attribute',
|
default: 'conversation_attribute',
|
||||||
},
|
},
|
||||||
|
contactId: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -55,15 +57,25 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async addAttribute(attribute) {
|
async addAttribute(attribute) {
|
||||||
const { attribute_key } = attribute;
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('updateCustomAttributes', {
|
const { attribute_key } = attribute;
|
||||||
conversationId: this.conversationId,
|
if (this.attributeType === 'conversation_attribute') {
|
||||||
customAttributes: {
|
await this.$store.dispatch('updateCustomAttributes', {
|
||||||
...this.customAttributes,
|
conversationId: this.conversationId,
|
||||||
[attribute_key]: null,
|
customAttributes: {
|
||||||
},
|
...this.customAttributes,
|
||||||
});
|
[attribute_key]: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.$store.dispatch('contacts/update', {
|
||||||
|
id: this.contactId,
|
||||||
|
custom_attributes: {
|
||||||
|
...this.customAttributes,
|
||||||
|
[attribute_key]: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
bus.$emit(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, attribute_key);
|
bus.$emit(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, attribute_key);
|
||||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.ADD.SUCCESS'));
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.ADD.SUCCESS'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -37,14 +37,23 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
contactId: { type: Number, default: null },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onUpdate(key, value) {
|
async onUpdate(key, value) {
|
||||||
|
const updatedAttributes = { ...this.customAttributes, [key]: value };
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('updateCustomAttributes', {
|
if (this.attributeType === 'conversation_attribute') {
|
||||||
conversationId: this.conversationId,
|
await this.$store.dispatch('updateCustomAttributes', {
|
||||||
customAttributes: { ...this.customAttributes, [key]: value },
|
conversationId: this.conversationId,
|
||||||
});
|
customAttributes: updatedAttributes,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('contacts/update', {
|
||||||
|
id: this.contactId,
|
||||||
|
custom_attributes: updatedAttributes,
|
||||||
|
});
|
||||||
|
}
|
||||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@@ -54,13 +63,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onDelete(key) {
|
async onDelete(key) {
|
||||||
const { [key]: remove, ...updatedAttributes } = this.customAttributes;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('updateCustomAttributes', {
|
const { [key]: remove, ...updatedAttributes } = this.customAttributes;
|
||||||
conversationId: this.conversationId,
|
if (this.attributeType === 'conversation_attribute') {
|
||||||
customAttributes: updatedAttributes,
|
await this.$store.dispatch('updateCustomAttributes', {
|
||||||
});
|
conversationId: this.conversationId,
|
||||||
|
customAttributes: updatedAttributes,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('contacts/deleteCustomAttributes', {
|
||||||
|
id: this.contactId,
|
||||||
|
customAttributes: [key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
|
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.NAME.PLACEHOLDER')"
|
||||||
|
@input="onDisplayNameChange"
|
||||||
@blur="$v.displayName.$touch"
|
@blur="$v.displayName.$touch"
|
||||||
/>
|
/>
|
||||||
<label :class="{ error: $v.description.$error }">
|
<label :class="{ error: $v.description.$error }">
|
||||||
@@ -53,22 +54,22 @@
|
|||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="displayName" class="medium-12 columns">
|
<woot-input
|
||||||
<label>
|
v-model="attributeKey"
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL') }}
|
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
|
||||||
<i class="ion-help" />
|
type="text"
|
||||||
</label>
|
:class="{ error: $v.attributeKey.$error }"
|
||||||
<p class="key-value text-truncate">
|
:error="
|
||||||
{{ attributeKey }}
|
$v.attributeKey.$error
|
||||||
</p>
|
? $t('ATTRIBUTES_MGMT.ADD.FORM.KEY.ERROR')
|
||||||
</div>
|
: ''
|
||||||
|
"
|
||||||
|
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
|
||||||
|
@blur="$v.attributeKey.$touch"
|
||||||
|
/>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
:disabled="
|
:disabled="isButtonDisabled"
|
||||||
$v.displayName.$invalid ||
|
|
||||||
$v.description.$invalid ||
|
|
||||||
uiFlags.isCreating
|
|
||||||
"
|
|
||||||
:button-text="$t('ATTRIBUTES_MGMT.ADD.SUBMIT')"
|
:button-text="$t('ATTRIBUTES_MGMT.ADD.SUBMIT')"
|
||||||
/>
|
/>
|
||||||
<button class="button clear" @click.prevent="onClose">
|
<button class="button clear" @click.prevent="onClose">
|
||||||
@@ -103,6 +104,7 @@ export default {
|
|||||||
description: '',
|
description: '',
|
||||||
attributeModel: 0,
|
attributeModel: 0,
|
||||||
attributeType: 0,
|
attributeType: 0,
|
||||||
|
attributeKey: '',
|
||||||
models: ATTRIBUTE_MODELS,
|
models: ATTRIBUTE_MODELS,
|
||||||
types: ATTRIBUTE_TYPES,
|
types: ATTRIBUTE_TYPES,
|
||||||
show: true,
|
show: true,
|
||||||
@@ -113,8 +115,12 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
uiFlags: 'getUIFlags',
|
uiFlags: 'getUIFlags',
|
||||||
}),
|
}),
|
||||||
attributeKey() {
|
isButtonDisabled() {
|
||||||
return convertToSlug(this.displayName);
|
return (
|
||||||
|
this.$v.displayName.$invalid ||
|
||||||
|
this.$v.description.$invalid ||
|
||||||
|
this.uiFlags.isCreating
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,9 +138,15 @@ export default {
|
|||||||
attributeType: {
|
attributeType: {
|
||||||
required,
|
required,
|
||||||
},
|
},
|
||||||
|
attributeKey: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onDisplayNameChange() {
|
||||||
|
this.attributeKey = convertToSlug(this.displayName);
|
||||||
|
},
|
||||||
async addAttributes() {
|
async addAttributes() {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('attributes/create', {
|
await this.$store.dispatch('attributes/create', {
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ 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() {
|
||||||
|
|||||||
@@ -41,15 +41,20 @@
|
|||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.TYPE.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="displayName" class="medium-12 columns">
|
<woot-input
|
||||||
<label>
|
v-model.trim="attributeKey"
|
||||||
{{ $t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL') }}
|
:label="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.LABEL')"
|
||||||
<i class="ion-help" />
|
type="text"
|
||||||
</label>
|
:class="{ error: $v.attributeKey.$error }"
|
||||||
<p class="key-value text-truncate">
|
:error="
|
||||||
{{ attributeKey }}
|
$v.attributeKey.$error
|
||||||
</p>
|
? $t('ATTRIBUTES_MGMT.ADD.FORM.KEY.ERROR')
|
||||||
</div>
|
: ''
|
||||||
|
"
|
||||||
|
:placeholder="$t('ATTRIBUTES_MGMT.ADD.FORM.KEY.PLACEHOLDER')"
|
||||||
|
readonly
|
||||||
|
@blur="$v.attributeKey.$touch"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<woot-button
|
<woot-button
|
||||||
@@ -69,7 +74,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
import { convertToSlug } from 'dashboard/helper/commons.js';
|
|
||||||
import { ATTRIBUTE_TYPES } from './constants';
|
import { ATTRIBUTE_TYPES } from './constants';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
export default {
|
export default {
|
||||||
@@ -92,6 +96,7 @@ export default {
|
|||||||
attributeType: 0,
|
attributeType: 0,
|
||||||
types: ATTRIBUTE_TYPES,
|
types: ATTRIBUTE_TYPES,
|
||||||
show: true,
|
show: true,
|
||||||
|
attributeKey: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
@@ -105,6 +110,9 @@ export default {
|
|||||||
required,
|
required,
|
||||||
minLength: minLength(1),
|
minLength: minLength(1),
|
||||||
},
|
},
|
||||||
|
attributeKey: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@@ -115,9 +123,6 @@ export default {
|
|||||||
this.selectedAttribute.attribute_display_name
|
this.selectedAttribute.attribute_display_name
|
||||||
}`;
|
}`;
|
||||||
},
|
},
|
||||||
attributeKey() {
|
|
||||||
return convertToSlug(this.displayName);
|
|
||||||
},
|
|
||||||
selectedAttributeType() {
|
selectedAttributeType() {
|
||||||
return this.types.find(
|
return this.types.find(
|
||||||
item =>
|
item =>
|
||||||
@@ -137,6 +142,7 @@ export default {
|
|||||||
this.displayName = this.selectedAttribute.attribute_display_name;
|
this.displayName = this.selectedAttribute.attribute_display_name;
|
||||||
this.description = this.selectedAttribute.attribute_description;
|
this.description = this.selectedAttribute.attribute_description;
|
||||||
this.attributeType = this.selectedAttributeType;
|
this.attributeType = this.selectedAttributeType;
|
||||||
|
this.attributeKey = this.selectedAttribute.attribute_key;
|
||||||
},
|
},
|
||||||
async editAttributes() {
|
async editAttributes() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
|||||||
export default {
|
export default {
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/settings/attributes'),
|
path: frontendURL('accounts/:accountId/settings/custom-attributes'),
|
||||||
component: SettingsContent,
|
component: SettingsContent,
|
||||||
props: {
|
props: {
|
||||||
headerTitle: 'ATTRIBUTES_MGMT.HEADER',
|
headerTitle: 'ATTRIBUTES_MGMT.HEADER',
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ export const ATTRIBUTE_MODELS = [
|
|||||||
id: 0,
|
id: 0,
|
||||||
option: 'Conversation',
|
option: 'Conversation',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
option: 'Contact',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ATTRIBUTE_TYPES = [
|
export const ATTRIBUTE_TYPES = [
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ export const getters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
get: async function getAttributesByModel({ commit }, modelId) {
|
get: async function getAttributesByModel({ commit }) {
|
||||||
commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true });
|
commit(types.SET_CUSTOM_ATTRIBUTE_UI_FLAG, { isFetching: true });
|
||||||
try {
|
try {
|
||||||
const response = await AttributeAPI.getAttributesByModel(modelId);
|
const response = await AttributeAPI.getAttributesByModel();
|
||||||
commit(types.SET_CUSTOM_ATTRIBUTE, response.data);
|
commit(types.SET_CUSTOM_ATTRIBUTE, response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
|
|||||||
@@ -110,6 +110,18 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteCustomAttributes: async ({ commit }, { id, customAttributes }) => {
|
||||||
|
try {
|
||||||
|
const response = await ContactAPI.destroyCustomAttributes(
|
||||||
|
id,
|
||||||
|
customAttributes
|
||||||
|
);
|
||||||
|
commit(types.EDIT_CONTACT, response.data.payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fetchContactableInbox: async ({ commit }, id) => {
|
fetchContactableInbox: async ({ commit }, id) => {
|
||||||
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
|
commit(types.SET_CONTACT_UI_FLAG, { isFetchingInboxes: true });
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -206,4 +206,24 @@ describe('#actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#deleteCustomAttributes', () => {
|
||||||
|
it('sends correct mutations if API is success', async () => {
|
||||||
|
axios.post.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||||
|
await actions.deleteCustomAttributes(
|
||||||
|
{ commit },
|
||||||
|
{ id: 1, customAttributes: ['cloud-customer'] }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([[types.EDIT_CONTACT, contactList[0]]]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(
|
||||||
|
actions.deleteCustomAttributes(
|
||||||
|
{ commit },
|
||||||
|
{ id: 1, customAttributes: ['cloud-customer'] }
|
||||||
|
)
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
# index_custom_attribute_definitions_on_account_id (account_id)
|
# index_custom_attribute_definitions_on_account_id (account_id)
|
||||||
#
|
#
|
||||||
class CustomAttributeDefinition < ApplicationRecord
|
class CustomAttributeDefinition < ApplicationRecord
|
||||||
|
scope :with_attribute_model, ->(attribute_model) { attribute_model.presence && where(attribute_model: attribute_model) }
|
||||||
validates :attribute_display_name, presence: true
|
validates :attribute_display_name, presence: true
|
||||||
|
|
||||||
validates :attribute_key,
|
validates :attribute_key,
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class ContactPolicy < ApplicationPolicy
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy_custom_attributes?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def show?
|
def show?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
json.payload do
|
||||||
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact, with_contact_inboxes: true
|
||||||
|
end
|
||||||
@@ -88,6 +88,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
member do
|
member do
|
||||||
get :contactable_inboxes
|
get :contactable_inboxes
|
||||||
|
post :destroy_custom_attributes
|
||||||
end
|
end
|
||||||
scope module: :contacts do
|
scope module: :contacts do
|
||||||
resources :conversations, only: [:index]
|
resources :conversations, only: [:index]
|
||||||
@@ -183,7 +184,11 @@ Rails.application.routes.draw do
|
|||||||
post :transcript
|
post :transcript
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resource :contact, only: [:show, :update]
|
resource :contact, only: [:show, :update] do
|
||||||
|
collection do
|
||||||
|
delete :destroy_custom_attributes
|
||||||
|
end
|
||||||
|
end
|
||||||
resources :inbox_members, only: [:index]
|
resources :inbox_members, only: [:index]
|
||||||
resources :labels, only: [:create, :destroy]
|
resources :labels, only: [:create, :destroy]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -501,4 +501,33 @@ RSpec.describe 'Contacts API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/accounts/{account.id}/contacts/:id/destroy_custom_attributes' do
|
||||||
|
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
||||||
|
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) }
|
||||||
|
let(:valid_params) { { custom_attributes: ['test'] } }
|
||||||
|
|
||||||
|
context 'when it is an unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/destroy_custom_attributes",
|
||||||
|
params: valid_params
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
|
||||||
|
it 'delete the custom attribute' do
|
||||||
|
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/destroy_custom_attributes",
|
||||||
|
headers: admin.create_new_auth_token,
|
||||||
|
params: valid_params,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(contact.reload.custom_attributes).to eq({ 'test1' => 'test1' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ RSpec.describe 'Custom Attribute Definitions API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
response_body = JSON.parse(response.body)
|
response_body = JSON.parse(response.body)
|
||||||
|
|
||||||
expect(response_body.count).to eq(1)
|
expect(response_body.count).to eq(2)
|
||||||
expect(response_body.first['attribute_key']).to eq(custom_attribute_definition.attribute_key)
|
expect(response_body.first['attribute_key']).to eq(custom_attribute_definition.attribute_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user