feat: Custom Attributes for contacts (#1158)
Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
parent
507b40a51d
commit
cdd385b269
18 changed files with 182 additions and 21 deletions
|
@ -33,7 +33,8 @@ class ContactIdentifyAction
|
|||
end
|
||||
|
||||
def update_contact
|
||||
@contact.update!(params.slice(:name, :email, :identifier))
|
||||
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
|
||||
@contact.update!(params.slice(:name, :email, :identifier).merge({ custom_attributes: custom_attributes }))
|
||||
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
|
||||
end
|
||||
|
||||
|
|
|
@ -12,14 +12,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = Current.account.contacts.new(contact_create_params)
|
||||
@contact = Current.account.contacts.new(contact_params)
|
||||
@contact.save!
|
||||
@contact_inbox = build_contact_inbox
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@contact.update!(contact_params)
|
||||
@contact.update!(contact_update_params)
|
||||
end
|
||||
|
||||
def search
|
||||
|
@ -43,14 +43,21 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number)
|
||||
params.require(:contact).permit(:name, :email, :phone_number, custom_attributes: {})
|
||||
end
|
||||
|
||||
def contact_custom_attributes
|
||||
return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes]
|
||||
|
||||
@contact.custom_attributes
|
||||
end
|
||||
|
||||
def contact_update_params
|
||||
# we want the merged custom attributes not the original one
|
||||
contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes })
|
||||
end
|
||||
|
||||
def fetch_contact
|
||||
@contact = Current.account.contacts.find(params[:id])
|
||||
end
|
||||
|
||||
def contact_create_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
|||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:website_token, :identifier, :email, :name, :avatar_url)
|
||||
params.permit(:website_token, :identifier, :email, :name, :avatar_url, custom_attributes: {})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
||||
"TITLE": "Previous Conversations"
|
||||
},
|
||||
"CUSTOM_ATTRIBUTES": {
|
||||
"TITLE": "Custom Attributes"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Conversation Labels",
|
||||
"MODAL": {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<contact-details-item
|
||||
:title="$t('CONTACT_PANEL.CUSTOM_ATTRIBUTES.TITLE')"
|
||||
icon="ion-code"
|
||||
/>
|
||||
<div
|
||||
v-for="attribute in listOfAttributes"
|
||||
:key="attribute"
|
||||
class="custom-attribute--row"
|
||||
>
|
||||
<div class="custom-attribute--row__attribute">
|
||||
{{ attribute }}
|
||||
</div>
|
||||
<div>
|
||||
{{ customAttributes[attribute] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactDetailsItem,
|
||||
},
|
||||
props: {
|
||||
customAttributes: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
listOfAttributes() {
|
||||
return Object.keys(this.customAttributes).filter(key => {
|
||||
const value = this.customAttributes[key];
|
||||
return value !== null && value !== undefined && value !== '';
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-attributes--panel {
|
||||
border-top: 1px solid var(--b-100);
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.custom-attribute--row {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.custom-attribute--row__attribute {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
|
@ -36,7 +36,7 @@ export default {
|
|||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.conv-details--item {
|
||||
padding-bottom: $space-normal;
|
||||
padding-bottom: var(--space-slab);
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
|
|
|
@ -84,6 +84,10 @@
|
|||
icon="ion-clock"
|
||||
/>
|
||||
</div>
|
||||
<contact-custom-attributes
|
||||
v-if="hasContactAttributes"
|
||||
:custom-attributes="contact.custom_attributes"
|
||||
/>
|
||||
<conversation-labels :conversation-id="conversationId" />
|
||||
<contact-conversations
|
||||
v-if="contact.id"
|
||||
|
@ -99,9 +103,11 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|||
import ContactConversations from './ContactConversations.vue';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
import ContactCustomAttributes from './ContactCustomAttributes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactCustomAttributes,
|
||||
ContactConversations,
|
||||
ContactDetailsItem,
|
||||
ConversationLabels,
|
||||
|
@ -129,6 +135,10 @@ export default {
|
|||
additionalAttributes() {
|
||||
return this.currentConversationMetaData.additional_attributes || {};
|
||||
},
|
||||
hasContactAttributes() {
|
||||
const { custom_attributes: customAttributes } = this.contact;
|
||||
return customAttributes && Object.keys(customAttributes).length;
|
||||
},
|
||||
browser() {
|
||||
return this.additionalAttributes.browser || {};
|
||||
},
|
||||
|
|
|
@ -32,6 +32,24 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
}
|
||||
},
|
||||
|
||||
setCustomAttributes(customAttributes = {}) {
|
||||
if (!customAttributes || !Object.keys(customAttributes).length) {
|
||||
throw new Error('Custom attributes should have atleast one key');
|
||||
} else {
|
||||
IFrameHelper.sendMessage('set-custom-attributes', { customAttributes });
|
||||
}
|
||||
},
|
||||
|
||||
deleteCustomAttribute(customAttribute = '') {
|
||||
if (!customAttribute) {
|
||||
throw new Error('Custom attribute is required');
|
||||
} else {
|
||||
IFrameHelper.sendMessage('delete-custom-attribute', {
|
||||
customAttribute,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setLabel(label = '') {
|
||||
IFrameHelper.sendMessage('set-label', { label });
|
||||
},
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
|
||||
import Vue from 'vue';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { setHeader } from 'widget/helpers/axios';
|
||||
|
@ -99,6 +97,15 @@ export default {
|
|||
this.$store.dispatch('conversationLabels/destroy', message.label);
|
||||
} else if (message.event === 'set-user') {
|
||||
this.$store.dispatch('contacts/update', message);
|
||||
} else if (message.event === 'set-custom-attributes') {
|
||||
this.$store.dispatch(
|
||||
'contacts/setCustomAttributes',
|
||||
message.customAttributes
|
||||
);
|
||||
} else if (message.event === 'delete-custom-attribute') {
|
||||
this.$store.dispatch('contacts/setCustomAttributes', {
|
||||
[message.customAttribute]: null,
|
||||
});
|
||||
} else if (message.event === 'set-locale') {
|
||||
this.setLocale(message.locale);
|
||||
this.setBubbleLabel();
|
||||
|
|
|
@ -9,4 +9,9 @@ export default {
|
|||
...userObject,
|
||||
});
|
||||
},
|
||||
setCustomAttibutes(customAttributes = {}) {
|
||||
return API.patch(buildUrl('widget/contact'), {
|
||||
custom_attributes: customAttributes,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -17,6 +17,13 @@ export const actions = {
|
|||
// Ingore error
|
||||
}
|
||||
},
|
||||
setCustomAttributes: async (_, customAttributes = {}) => {
|
||||
try {
|
||||
await ContactsAPI.setCustomAttibutes(customAttributes);
|
||||
} catch (error) {
|
||||
// Ingore error
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#
|
||||
# id :integer not null, primary key
|
||||
# additional_attributes :jsonb
|
||||
# custom_attributes :jsonb
|
||||
# email :string
|
||||
# identifier :string
|
||||
# name :string
|
||||
|
@ -68,12 +69,12 @@ class Contact < ApplicationRecord
|
|||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def downcase_email
|
||||
email.downcase! if email.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dispatch_create_event
|
||||
Rails.configuration.dispatcher.dispatch(CONTACT_CREATED, Time.zone.now, contact: self)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ json.id resource.id
|
|||
json.name resource.name
|
||||
json.phone_number resource.phone_number
|
||||
json.thumbnail resource.avatar_url
|
||||
json.custom_attributes resource.custom_attributes
|
||||
|
||||
# we only want to output contact inbox when its /contacts endpoints
|
||||
if defined?(with_contact_inboxes) && with_contact_inboxes.present?
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddCustomAttributesToContacts < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :contacts, :custom_attributes, :jsonb, default: {}
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2020_08_02_170002) do
|
||||
ActiveRecord::Schema.define(version: 2020_08_19_190629) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
|
@ -204,6 +204,7 @@ ActiveRecord::Schema.define(version: 2020_08_02_170002) do
|
|||
t.string "pubsub_token"
|
||||
t.jsonb "additional_attributes"
|
||||
t.string "identifier"
|
||||
t.jsonb "custom_attributes", default: {}
|
||||
t.index ["account_id"], name: "index_contacts_on_account_id"
|
||||
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
|
||||
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
|
||||
|
|
|
@ -66,6 +66,31 @@ window.$chatwoot.setUser('identifier_key', {
|
|||
|
||||
Make sure that you reset the session when the user logs out of your app.
|
||||
|
||||
### Set custom attributes
|
||||
|
||||
Inorder to set additional information about the customer you can use customer attributes field.
|
||||
|
||||
To set a custom attributes call `setCustomAttributes` as follows
|
||||
|
||||
```js
|
||||
window.$chatwoot.setCustomAttributes({
|
||||
accountId: 1,
|
||||
pricingPlan: 'paid',
|
||||
|
||||
// You can pass any key value pair here.
|
||||
// Value should either be a string or a number.
|
||||
// You need to flatten nested JSON structure while using this function
|
||||
});
|
||||
```
|
||||
|
||||
You can view these information in the sidepanel of a conversation.
|
||||
|
||||
To delete a custom attribute, use `deleteCustomAttribute` as follows
|
||||
|
||||
```js
|
||||
window.$chatwoot.deleteCustomAttribute('attribute-name');
|
||||
```
|
||||
|
||||
### To set language manually
|
||||
|
||||
```js
|
||||
|
|
|
@ -4,14 +4,17 @@ describe ::ContactIdentifyAction do
|
|||
subject(:contact_identify) { described_class.new(contact: contact, params: params).perform }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let(:params) { { name: 'test', identifier: 'test_id' } }
|
||||
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
||||
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) }
|
||||
let(:params) { { name: 'test', identifier: 'test_id', custom_attributes: { test: 'new test', test2: 'test2' } } }
|
||||
|
||||
describe '#perform' do
|
||||
it 'updates the contact' do
|
||||
expect(ContactAvatarJob).not_to receive(:perform_later).with(contact, params[:avatar_url])
|
||||
contact_identify
|
||||
expect(contact.reload.name).to eq 'test'
|
||||
# custom attributes are merged properly without overwritting existing ones
|
||||
expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' })
|
||||
expect(contact.reload.identifier).to eq 'test_id'
|
||||
end
|
||||
|
||||
|
|
|
@ -83,7 +83,8 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/contacts' do
|
||||
let(:valid_params) { { contact: { name: 'test' } } }
|
||||
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
||||
let(:valid_params) { { contact: { name: 'test', custom_attributes: custom_attributes } } }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
|
@ -104,6 +105,10 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
end.to change(Contact, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
# custom attributes are updated
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
|
||||
end
|
||||
|
||||
it 'creates the contact identifier when inbox id is passed' do
|
||||
|
@ -118,8 +123,9 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
end
|
||||
|
||||
describe 'PATCH /api/v1/accounts/{account.id}/contacts/:id' do
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let(:valid_params) { { contact: { name: 'Test Blub' } } }
|
||||
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
||||
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) }
|
||||
let(:valid_params) { { name: 'Test Blub', custom_attributes: { test: 'new test', test2: 'test2' } } }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
|
@ -140,7 +146,9 @@ RSpec.describe 'Contacts API', type: :request do
|
|||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Contact.last.name).to eq('Test Blub')
|
||||
expect(contact.reload.name).to eq('Test Blub')
|
||||
# custom attributes are merged properly without overwritting existing ones
|
||||
expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' })
|
||||
end
|
||||
|
||||
it 'prevents the update of contact of another account' do
|
||||
|
|
Loading…
Reference in a new issue