Merge branch 'develop' of github.com:chatwoot/chatwoot into feat/ui-lib
This commit is contained in:
@@ -75,6 +75,21 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
active: async ({ commit }, { page = 1, sortAttr } = {}) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await ContactAPI.active(page, sortAttr);
|
||||
commit(types.CLEAR_CONTACTS);
|
||||
commit(types.SET_CONTACTS, payload);
|
||||
commit(types.SET_CONTACT_META, meta);
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
show: async ({ commit }, { id }) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
|
||||
try {
|
||||
|
||||
@@ -327,6 +327,16 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteConversation: async ({ commit, dispatch }, conversationId) => {
|
||||
try {
|
||||
await ConversationApi.delete(conversationId);
|
||||
commit(types.DELETE_CONVERSATION, conversationId);
|
||||
dispatch('conversationStats/get', {}, { root: true });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
|
||||
addConversation({ commit, state, dispatch, rootState }, conversation) {
|
||||
const { currentInbox, appliedFilters } = state;
|
||||
const {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
* 3. Nested properties in custom_attributes (conversation_type, etc.)
|
||||
*/
|
||||
import jsonLogic from 'json-logic-js';
|
||||
import { coerceToDate } from '@chatwoot/utils';
|
||||
|
||||
/**
|
||||
* Gets a value from a conversation based on the attribute key
|
||||
@@ -157,6 +158,20 @@ const contains = (filterValue, conversationValue) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares two date values using a comparison function
|
||||
* @param {*} conversationValue - The conversation value to compare
|
||||
* @param {*} filterValue - The filter value to compare against
|
||||
* @param {Function} compareFn - The comparison function to apply
|
||||
* @returns {Boolean} - Returns true if the comparison succeeds, false otherwise
|
||||
*/
|
||||
const compareDates = (conversationValue, filterValue, compareFn) => {
|
||||
const conversationDate = coerceToDate(conversationValue);
|
||||
const filterDate = coerceToDate(filterValue);
|
||||
if (conversationDate === null || filterDate === null) return false;
|
||||
return compareFn(conversationDate, filterDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a value matches a filter condition
|
||||
* @param {*} conversationValue - The value to check
|
||||
@@ -195,10 +210,10 @@ const matchesCondition = (conversationValue, filter) => {
|
||||
return false; // We already handled null/undefined above
|
||||
|
||||
case 'is_greater_than':
|
||||
return new Date(conversationValue) > new Date(filterValue);
|
||||
return compareDates(conversationValue, filterValue, (a, b) => a > b);
|
||||
|
||||
case 'is_less_than':
|
||||
return new Date(conversationValue) < new Date(filterValue);
|
||||
return compareDates(conversationValue, filterValue, (a, b) => a < b);
|
||||
|
||||
case 'days_before': {
|
||||
const today = new Date();
|
||||
@@ -347,6 +362,7 @@ export const matchesFilters = (conversation, filters) => {
|
||||
conversation,
|
||||
filters[0].attribute_key
|
||||
);
|
||||
|
||||
return matchesCondition(value, filters[0]);
|
||||
}
|
||||
|
||||
|
||||
+235
@@ -463,6 +463,241 @@ describe('filterHelpers', () => {
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with 10-digit timestamp (seconds) vs standard date filter
|
||||
it('should match conversation with 10-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: 1647777600 }; // March 20, 2022 in seconds (10 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with 13-digit timestamp (milliseconds) vs standard date filter
|
||||
it('should match conversation with 13-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022 in milliseconds (13 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with string timestamp vs standard date filter
|
||||
it('should match conversation with string 10-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: '1647777600' }; // March 20, 2022 as string (10 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with string 13-digit timestamp vs standard date filter
|
||||
it('should match conversation with string 13-digit timestamp against date string filter', () => {
|
||||
const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string (13 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19', // Standard YYYY-MM-DD format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test conversation with mixed format vs standard date filter with time
|
||||
it('should match conversation with numeric timestamp against ISO date string filter', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022 12:00:00 GMT (numeric)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19T10:30:00Z', // Standard ISO format from filter
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with date string without time (should default to 00:00:00)
|
||||
it('should match conversation with is_greater_than operator using date string without time', () => {
|
||||
const conversation = { created_at: 1647820800000 }; // March 21, 2022 00:00:00 GMT
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-20', // March 20, 2022 (should become 00:00:00)
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with ISO date string
|
||||
it('should match conversation with is_greater_than operator using ISO date string', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19T00:00:00.000Z', // March 19, 2022 ISO format
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with null/undefined values
|
||||
it('should handle null filter values in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: null,
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined filter values in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: undefined,
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
// Test parseDate with invalid date strings
|
||||
it('should handle invalid date strings in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: 'invalid-date-string',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle non-date string values in date comparison', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: 'not-a-date',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
// Test is_less_than with various date formats
|
||||
it('should match conversation with is_less_than operator using numeric timestamp', () => {
|
||||
const conversation = { created_at: 1647691200000 }; // March 19, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_less_than',
|
||||
values: 1647777600, // March 20, 2022 as 10-digit timestamp
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match conversation with is_less_than operator when date is later', () => {
|
||||
const conversation = { created_at: 1647864000000 }; // March 21, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_less_than',
|
||||
values: '2022-03-20T12:00:00Z', // March 20, 2022 with time
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
// Edge case: Test with conversation having string timestamp
|
||||
it('should handle conversation with string timestamp value', () => {
|
||||
const conversation = { created_at: '1647777600000' }; // March 20, 2022 as string
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Edge case: Test with conversation having 10-digit timestamp
|
||||
it('should handle conversation with 10-digit timestamp value', () => {
|
||||
const conversation = { created_at: 1647777600 }; // March 20, 2022 as seconds (10 digits)
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19',
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test date string with different time formats
|
||||
it('should handle date string with space-separated time', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: '2022-03-19 10:30:00', // Date with space-separated time
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(true);
|
||||
});
|
||||
|
||||
// Test parseDate with object input (should return null and fail comparison)
|
||||
it('should handle non-string, non-number filter values', () => {
|
||||
const conversation = { created_at: 1647777600000 }; // March 20, 2022
|
||||
const filters = [
|
||||
{
|
||||
attribute_key: 'created_at',
|
||||
filter_operator: 'is_greater_than',
|
||||
values: { date: '2022-03-19' }, // Object instead of string/number
|
||||
query_operator: 'and',
|
||||
},
|
||||
];
|
||||
expect(matchesFilters(conversation, filters)).toBe(false);
|
||||
});
|
||||
|
||||
describe('days_before operator', () => {
|
||||
beforeEach(() => {
|
||||
// Set the date to March 25, 2022
|
||||
|
||||
@@ -204,6 +204,12 @@ export const mutations = {
|
||||
_state.allConversations.push(conversation);
|
||||
},
|
||||
|
||||
[types.DELETE_CONVERSATION](_state, conversationId) {
|
||||
_state.allConversations = _state.allConversations.filter(
|
||||
c => c.id !== conversationId
|
||||
);
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION](_state, conversation) {
|
||||
const { allConversations } = _state;
|
||||
const index = allConversations.findIndex(c => c.id === conversation.id);
|
||||
|
||||
@@ -70,6 +70,30 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#active', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: { payload: contactList, meta: { count: 100, current_page: 1 } },
|
||||
});
|
||||
await actions.active({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||
[types.CLEAR_CONTACTS],
|
||||
[types.SET_CONTACTS, contactList],
|
||||
[types.SET_CONTACT_META, { count: 100, current_page: 1 }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct mutations if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.active({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||
|
||||
@@ -513,6 +513,28 @@ describe('#deleteMessage', () => {
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
describe('#deleteConversation', () => {
|
||||
it('send correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({
|
||||
data: { id: 1 },
|
||||
});
|
||||
await actions.deleteConversation({ commit, dispatch }, 1);
|
||||
expect(commit.mock.calls).toEqual([[types.DELETE_CONVERSATION, 1]]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
['conversationStats/get', {}, { root: true }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('send no actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.deleteConversation({ commit, dispatch }, 1)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateCustomAttributes', () => {
|
||||
it('update conversation custom attributes', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
|
||||
@@ -884,6 +884,17 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_CONVERSATION', () => {
|
||||
it('should delete a conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [] }],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_CONVERSATION](state, 1);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_LIST_LOADING_STATUS', () => {
|
||||
it('should set listLoadingStatus to true', () => {
|
||||
const state = {
|
||||
|
||||
Reference in New Issue
Block a user