chore: Update deleteCustomAttribute method in SDK (#3334)

This commit is contained in:
Muhsin Keloth 2021-11-15 14:56:35 +05:30 committed by GitHub
parent c2db8a1fd7
commit a2764e5c1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 138 additions and 30 deletions

View file

@ -11,6 +11,13 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
render json: contact_identify_action.perform render json: contact_identify_action.perform
end end
# TODO : clean up this with proper routes delete contacts/custom_attributes
def destroy_custom_attributes
@contact.custom_attributes = @contact.custom_attributes.excluding(params[:custom_attributes])
@contact.save!
render json: @contact
end
private private
def process_hmac def process_hmac

View file

@ -99,7 +99,7 @@ export default {
isEditing: false, isEditing: false,
editedValue: editedValue:
this.attributeType === 'date' this.attributeType === 'date'
? format(new Date(this.value), DATE_FORMAT) ? format(new Date(this.value || new Date()), DATE_FORMAT)
: this.value, : this.value,
}; };
}, },

View file

@ -20,6 +20,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'conversation.contact_changed': this.onConversationContactChange, 'conversation.contact_changed': this.onConversationContactChange,
'presence.update': this.onPresenceUpdate, 'presence.update': this.onPresenceUpdate,
'contact.deleted': this.onContactDelete, 'contact.deleted': this.onContactDelete,
'contact.updated': this.onContactUpdate,
}; };
} }
@ -124,6 +125,10 @@ class ActionCableConnector extends BaseActionCableConnector {
); );
this.fetchConversationStats(); this.fetchConversationStats();
}; };
onContactUpdate = data => {
this.app.$store.dispatch('contacts/updateContact', data);
};
} }
export default { export default {

View file

@ -29,6 +29,7 @@ export default {
conversationId() { conversationId() {
return this.currentChat.id; return this.currentChat.id;
}, },
filteredAttributes() { filteredAttributes() {
return Object.keys(this.customAttributes).map(key => { return Object.keys(this.customAttributes).map(key => {
const item = this.attributes.find( const item = this.attributes.find(
@ -38,15 +39,17 @@ export default {
return { return {
...item, ...item,
value: this.customAttributes[key], value: this.customAttributes[key],
icon: this.attributeIcon(item.attribute_display_type),
}; };
} }
return { return {
...item, ...item,
value: this.customAttributes[key], value: this.customAttributes[key],
attribute_description: key, attribute_description: key,
attribute_display_name: key, attribute_display_name: key,
attribute_display_type: 'text', attribute_display_type: this.attributeDisplayType(
this.customAttributes[key]
),
attribute_key: key, attribute_key: key,
attribute_model: this.attributeType, attribute_model: this.attributeType,
id: Math.random(), id: Math.random(),
@ -55,21 +58,24 @@ export default {
}, },
}, },
methods: { methods: {
attributeIcon(attributeType) { isAttributeNumber(attributeValue) {
switch (attributeType) { return (
case 'date': Number.isInteger(Number(attributeValue)) && Number(attributeValue) > 0
return 'ion-calendar'; );
case 'link': },
return 'ion-link'; isAttributeLink(attributeValue) {
case 'currency': /* eslint-disable no-useless-escape */
return 'ion-social-usd'; const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
case 'number': return URL_REGEX.test(attributeValue);
return 'ion-calculator'; },
case 'percent': attributeDisplayType(attributeValue) {
return 'ion-calculator'; if (this.isAttributeNumber(attributeValue)) {
default: return 'number';
return 'ion-edit';
} }
if (this.isAttributeLink(attributeValue)) {
return 'link';
}
return 'text';
}, },
}, },
}; };

View file

@ -27,7 +27,7 @@ describe('attributeMixin', () => {
}), }),
getCurrentAccountId: () => 1, getCurrentAccountId: () => 1,
attributeType: () => 'conversation_attribute', attributeType: () => 'conversation_attribute',
}; };
store = new Vuex.Store({ actions, getters }); store = new Vuex.Store({ actions, getters });
}); });
@ -78,15 +78,40 @@ describe('attributeMixin', () => {
]); ]);
}); });
it('return icon if attribute type passed correctly', () => { it('return display type if attribute passed', () => {
const Component = { const Component = {
render() {}, render() {},
title: 'TestComponent', title: 'TestComponent',
mixins: [attributeMixin], mixins: [attributeMixin],
}; };
const wrapper = shallowMount(Component, { store, localVue }); const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.attributeIcon('date')).toBe('ion-calendar'); expect(wrapper.vm.attributeDisplayType('date')).toBe('text');
expect(wrapper.vm.attributeIcon()).toBe('ion-edit'); expect(
wrapper.vm.attributeDisplayType('https://www.chatwoot.com/pricing')
).toBe('link');
expect(wrapper.vm.attributeDisplayType(9988)).toBe('number');
});
it('return true if link is passed', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [attributeMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.isAttributeLink('https://www.chatwoot.com/pricing')).toBe(
true
);
});
it('return true if number is passed', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [attributeMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.isAttributeNumber(9988)).toBe(true);
}); });
it('returns currently selected contact', () => { it('returns currently selected contact', () => {

View file

@ -169,4 +169,14 @@ export const actions = {
root: true, root: true,
}); });
}, },
updateContact: async ({ commit }, updateObj) => {
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: true });
try {
commit(types.EDIT_CONTACT, updateObj);
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
} catch (error) {
commit(types.SET_CONTACT_UI_FLAG, { isUpdating: false });
}
},
}; };

View file

@ -207,7 +207,28 @@ describe('#actions', () => {
}); });
}); });
describe('#deleteCustomAttributes', () => { describe('#updateContact', () => {
it('sends correct mutations if API is success', async () => {
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
await actions.updateContact({ commit }, contactList[0]);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isUpdating: true }],
[types.EDIT_CONTACT, contactList[0]],
[types.SET_CONTACT_UI_FLAG, { isUpdating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.update({ commit }, contactList[0])).rejects.toThrow(
Error
);
expect(commit.mock.calls).toEqual([
[types.SET_CONTACT_UI_FLAG, { isUpdating: true }],
[types.SET_CONTACT_UI_FLAG, { isUpdating: false }],
]);
});
});
describe('#deleteCustomAttributes', () => {
it('sends correct mutations if API is success', async () => { it('sends correct mutations if API is success', async () => {
axios.post.mockResolvedValue({ data: { payload: contactList[0] } }); axios.post.mockResolvedValue({ data: { payload: contactList[0] } });
await actions.deleteCustomAttributes( await actions.deleteCustomAttributes(

View file

@ -262,9 +262,10 @@ export default {
message.customAttributes message.customAttributes
); );
} else if (message.event === 'delete-custom-attribute') { } else if (message.event === 'delete-custom-attribute') {
this.$store.dispatch('contacts/setCustomAttributes', { this.$store.dispatch(
[message.customAttribute]: null, 'contacts/deleteCustomAttribute',
}); message.customAttribute
);
} else if (message.event === 'set-locale') { } else if (message.event === 'set-locale') {
this.setLocale(message.locale); this.setLocale(message.locale);
this.setBubbleLabel(); this.setBubbleLabel();

View file

@ -12,9 +12,14 @@ export default {
...userObject, ...userObject,
}); });
}, },
setCustomAttibutes(customAttributes = {}) { setCustomAttributes(customAttributes = {}) {
return API.patch(buildUrl('widget/contact'), { return API.patch(buildUrl('widget/contact'), {
custom_attributes: customAttributes, custom_attributes: customAttributes,
}); });
}, },
deleteCustomAttribute(customAttribute) {
return API.post(buildUrl('widget/contact/destroy_custom_attributes'), {
custom_attributes: [customAttribute],
});
},
}; };

View file

@ -43,14 +43,21 @@ export const actions = {
refreshActionCableConnector(pubsubToken); refreshActionCableConnector(pubsubToken);
} catch (error) { } catch (error) {
// Ingore error // Ignore error
} }
}, },
setCustomAttributes: async (_, customAttributes = {}) => { setCustomAttributes: async (_, customAttributes = {}) => {
try { try {
await ContactsAPI.setCustomAttibutes(customAttributes); await ContactsAPI.setCustomAttributes(customAttributes);
} catch (error) { } catch (error) {
// Ingore error // Ignore error
}
},
deleteCustomAttribute: async (_, customAttribute) => {
try {
await ContactsAPI.deleteCustomAttribute(customAttribute);
} catch (error) {
// Ignore error
} }
}, },
}; };

View file

@ -186,7 +186,7 @@ Rails.application.routes.draw do
end end
resource :contact, only: [:show, :update] do resource :contact, only: [:show, :update] do
collection do collection do
delete :destroy_custom_attributes post :destroy_custom_attributes
end end
end end
resources :inbox_members, only: [:index] resources :inbox_members, only: [:index]

View file

@ -87,4 +87,25 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
end end
end end
end end
describe 'POST /api/v1/widget/destroy_custom_attributes' do
let(:params) { { website_token: web_widget.website_token, identifier: 'test', custom_attributes: ['test'] } }
context 'with invalid website token' do
it 'returns unauthorized' do
post '/api/v1/widget/destroy_custom_attributes', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token' do
it 'calls destroy custom attributes' do
post '/api/v1/widget/destroy_custom_attributes',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(contact.reload.custom_attributes).to eq({})
end
end
end
end end