diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js index 2591c2c01..d64d3c56b 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js @@ -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]); } diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js index 6d709f085..0c9e6a5c0 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js @@ -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 diff --git a/package.json b/package.json index 6280ec7fd..eb5098fa0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", "@chatwoot/prosemirror-schema": "1.1.1-next", - "@chatwoot/utils": "^0.0.45", + "@chatwoot/utils": "^0.0.46", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", "@hcaptcha/vue3-hcaptcha": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 606ebbb63..db7589569 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: 1.1.1-next version: 1.1.1-next '@chatwoot/utils': - specifier: ^0.0.45 - version: 0.0.45 + specifier: ^0.0.46 + version: 0.0.46 '@formkit/core': specifier: ^1.6.7 version: 1.6.7 @@ -406,8 +406,8 @@ packages: '@chatwoot/prosemirror-schema@1.1.1-next': resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==} - '@chatwoot/utils@0.0.45': - resolution: {integrity: sha512-zqmuri6MrEFAY1tLv7Z3HBy4Ig60LhSrLkEiHegVsOVSxPv4Bedq+xmAW7LphvcLNgbkkvu17MU91gvMVlpEHw==} + '@chatwoot/utils@0.0.46': + resolution: {integrity: sha512-a68CQ+aPFfyMr7dnXUUSt/kwHEazBd7Y8aidDZeDp5eL7sych7EpmT5XMTmhttlqMiRsmwETblXJJ2fBH6I44A==} engines: {node: '>=10'} '@codemirror/commands@6.7.0': @@ -5255,7 +5255,7 @@ snapshots: prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3) prosemirror-view: 1.34.1 - '@chatwoot/utils@0.0.45': + '@chatwoot/utils@0.0.46': dependencies: date-fns: 2.30.0