Skip to content

Commit 678cd71

Browse files
Merge pull request #4662 from OneCommunityGlobal/fix/feedback-search-undefined-parameters
Siva - Fix feedback search
2 parents 78777e6 + 2816db9 commit 678cd71

4 files changed

Lines changed: 188 additions & 16 deletions

File tree

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
*.png
22
*.svg
33
src/actions/**
4-
src/App.css
54
src/config.json
65

76
src/languages/**

src/components/CommunityPortal/Activities/activityId/ActivityComments.jsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState, useEffect } from 'react';
2+
import { useSelector } from 'react-redux';
23
import styles from './ActivityComments.module.css';
34

45
// Utility function to calculate relative time
@@ -81,7 +82,6 @@ const sanitizeInput = input => {
8182
return result.trim().substring(0, 5000); // Limit length to prevent DoS attacks
8283
};
8384

84-
// Utility function to generate secure random numbers for demo purposes
8585
// Utility function to generate secure random numbers for demo purposes
8686
const getSecureRandomInt = (min, max) => {
8787
// Use a deterministic approach for demo data instead of Math.random()
@@ -244,6 +244,8 @@ const mockFeedbacks = [
244244
];
245245

246246
function ActivityComments() {
247+
const darkMode = useSelector(state => state.theme?.darkMode || false);
248+
247249
// Utility function to restore Date objects from localStorage
248250
const restoreDates = items => {
249251
return items.map(item => ({
@@ -632,20 +634,44 @@ function ActivityComments() {
632634
);
633635
};
634636

637+
/**
638+
* Filter and sort feedbacks based on search, filter, and sort criteria
639+
*
640+
* Search Parameters:
641+
* - Reviewer name (feedback.name): Searches in the reviewer's name
642+
* - Feedback text (feedback.text): Searches in the feedback comment/description
643+
*
644+
* The search uses case-insensitive partial matching (contains) for both fields.
645+
*/
635646
const filteredFeedbacks = feedbacks
636647
.filter(feedback => {
637-
const matchesSearch =
638-
feedback.text.toLowerCase().includes(feedbackSearch.toLowerCase()) ||
639-
feedback.name.toLowerCase().includes(feedbackSearch.toLowerCase());
648+
// Search logic: check if search term matches reviewer name or feedback text
649+
const searchTerm = feedbackSearch.trim().toLowerCase();
650+
let matchesSearch = true;
651+
652+
if (searchTerm) {
653+
const reviewerName = (feedback.name || '').toLowerCase();
654+
const feedbackText = (feedback.text || '').toLowerCase();
655+
656+
// Explicit search matching: check both fields with OR logic
657+
matchesSearch = reviewerName.includes(searchTerm) || feedbackText.includes(searchTerm);
658+
}
659+
660+
// Rating filter logic
640661
const matchesFilter =
641662
feedbackFilter === 'All' || feedback.rating.toString() === feedbackFilter;
663+
642664
return matchesSearch && matchesFilter;
643665
})
644666
.sort((a, b) => {
645-
if (feedbackSort === 'Oldest') return new Date(a.timestamp) - new Date(b.timestamp);
667+
// Sort by creation date with null safety
668+
const dateA = a.createdAt ? new Date(a.createdAt) : new Date(0);
669+
const dateB = b.createdAt ? new Date(b.createdAt) : new Date(0);
670+
671+
if (feedbackSort === 'Oldest') return dateA - dateB;
646672
if (feedbackSort === 'Highest Rated') return b.rating - a.rating;
647673
if (feedbackSort === 'Lowest Rated') return a.rating - b.rating;
648-
return new Date(b.timestamp) - new Date(a.timestamp); // Newest
674+
return dateB - dateA; // Newest (default)
649675
});
650676

651677
return (
@@ -1117,16 +1143,18 @@ function ActivityComments() {
11171143
>
11181144
<input
11191145
type="text"
1120-
placeholder="Search feedback..."
1146+
placeholder="Search by reviewer name or feedback text..."
11211147
value={feedbackSearch}
11221148
onChange={e => setFeedbackSearch(e.target.value)}
11231149
style={{
11241150
flex: 1,
11251151
minWidth: '200px',
11261152
padding: '8px 12px',
1127-
border: '1px solid #ddd',
1153+
border: darkMode ? '1px solid #4a5a77' : '1px solid #ddd',
11281154
borderRadius: '6px',
11291155
fontSize: '0.9rem',
1156+
backgroundColor: darkMode ? '#3a506b' : '#fff',
1157+
color: darkMode ? '#ffffff' : '#222',
11301158
}}
11311159
/>
11321160
<select
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Unit tests for Feedback search functionality in ActivityComments component
3+
* Tests search by reviewer name and feedback text with case-insensitive partial matching
4+
*/
5+
6+
describe('ActivityComments - Feedback Search Functionality', () => {
7+
const mockFeedbacks = [
8+
{
9+
id: 1,
10+
name: 'Sarah Johnson',
11+
text: 'This was an absolutely fantastic event!',
12+
rating: 5,
13+
createdAt: new Date('2024-01-01'),
14+
},
15+
{
16+
id: 2,
17+
name: 'Anonymous User',
18+
text: 'Really enjoyed the event overall.',
19+
rating: 4,
20+
createdAt: new Date('2024-01-02'),
21+
},
22+
{
23+
id: 3,
24+
name: 'Mike Chen',
25+
text: 'The event was okay. Some parts were interesting.',
26+
rating: 3,
27+
createdAt: new Date('2024-01-03'),
28+
},
29+
];
30+
31+
// Helper function to simulate the search filter logic
32+
const filterFeedbacks = (feedbacks, searchTerm, filter = 'All') => {
33+
return feedbacks.filter(feedback => {
34+
const trimmedSearch = searchTerm.trim().toLowerCase();
35+
let matchesSearch = true;
36+
37+
if (trimmedSearch) {
38+
const reviewerName = (feedback.name || '').toLowerCase();
39+
const feedbackText = (feedback.text || '').toLowerCase();
40+
matchesSearch =
41+
reviewerName.includes(trimmedSearch) || feedbackText.includes(trimmedSearch);
42+
}
43+
44+
const matchesFilter = filter === 'All' || feedback.rating.toString() === filter;
45+
return matchesSearch && matchesFilter;
46+
});
47+
};
48+
49+
// Helper function for common assertions
50+
const expectSingleResult = (results, expectedName, expectedText) => {
51+
expect(results).toHaveLength(1);
52+
if (expectedName) expect(results[0].name).toBe(expectedName);
53+
if (expectedText) expect(results[0].text).toContain(expectedText);
54+
};
55+
56+
describe('Search by reviewer name', () => {
57+
test.each([
58+
['Sarah Johnson', 'Sarah Johnson', null],
59+
['Sarah', 'Sarah Johnson', null],
60+
['sarah', 'Sarah Johnson', null],
61+
['Mike', 'Mike Chen', null],
62+
])('should find feedback for search term "%s"', (searchTerm, expectedName) => {
63+
const results = filterFeedbacks(mockFeedbacks, searchTerm);
64+
expectSingleResult(results, expectedName, null);
65+
});
66+
});
67+
68+
describe('Search by feedback text', () => {
69+
test.each([
70+
['fantastic event', null, 'fantastic'],
71+
['enjoyed', null, 'enjoyed'],
72+
['FANTASTIC', null, 'fantastic'],
73+
])('should find feedback for text search "%s"', (searchTerm, expectedName, expectedText) => {
74+
const results = filterFeedbacks(mockFeedbacks, searchTerm);
75+
expectSingleResult(results, expectedName, expectedText);
76+
});
77+
});
78+
79+
describe('Search edge cases', () => {
80+
test.each([
81+
['', 3],
82+
[' ', 3],
83+
['nonexistent', 0],
84+
])('should return %d results for search term "%s"', (searchTerm, expectedCount) => {
85+
const results = filterFeedbacks(mockFeedbacks, searchTerm);
86+
expect(results).toHaveLength(expectedCount);
87+
});
88+
89+
test('should handle null or undefined name gracefully', () => {
90+
const feedbackWithNullName = {
91+
id: 4,
92+
name: null,
93+
text: 'Some feedback text',
94+
rating: 5,
95+
createdAt: new Date('2024-01-04'),
96+
};
97+
const results = filterFeedbacks([feedbackWithNullName], 'text');
98+
expect(results).toHaveLength(1);
99+
});
100+
101+
test('should handle null or undefined text gracefully', () => {
102+
const feedbackWithNullText = {
103+
id: 5,
104+
name: 'John Doe',
105+
text: null,
106+
rating: 5,
107+
createdAt: new Date('2024-01-05'),
108+
};
109+
const results = filterFeedbacks([feedbackWithNullText], 'John');
110+
expect(results).toHaveLength(1);
111+
});
112+
});
113+
114+
describe('Search with rating filter', () => {
115+
test.each([
116+
['event', '5', 1, 5],
117+
['Sarah', '1', 0, null],
118+
])(
119+
'should filter by rating %s and search "%s" to return %d results',
120+
(searchTerm, rating, expectedCount, expectedRating) => {
121+
const results = filterFeedbacks(mockFeedbacks, searchTerm, rating);
122+
expect(results).toHaveLength(expectedCount);
123+
if (expectedRating) expect(results[0].rating).toBe(expectedRating);
124+
},
125+
);
126+
});
127+
128+
describe('Partial matching', () => {
129+
test.each([
130+
['John', 'name', 'Johnson'],
131+
['absolutely', 'text', 'absolutely'],
132+
])('should match partial words in %s', (searchTerm, field, expectedContent) => {
133+
const results = filterFeedbacks(mockFeedbacks, searchTerm);
134+
expect(results).toHaveLength(1);
135+
expect(results[0][field]).toContain(expectedContent);
136+
});
137+
});
138+
});

src/utils/authInit.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@ export default function initAuth() {
1111
const token = localStorage.getItem(config.tokenKey);
1212
if (!token) return;
1313

14-
const decoded = jwtDecode(token);
15-
const nowSec = Date.now() / 1000;
16-
const expirySec = new Date(decoded.expiryTimestamp).getTime() / 1000;
14+
try {
15+
const decoded = jwtDecode(token);
16+
const nowSec = Date.now() / 1000;
17+
const expirySec = new Date(decoded.expiryTimestamp).getTime() / 1000;
1718

18-
if (expirySec - TOKEN_LIFETIME_BUFFER < nowSec) {
19+
if (expirySec - TOKEN_LIFETIME_BUFFER < nowSec) {
20+
store.dispatch(logoutUser());
21+
} else {
22+
httpService.setjwt(token);
23+
store.dispatch(setCurrentUser(decoded));
24+
}
25+
} catch (error) {
26+
// Handle invalid or malformed token
27+
console.error('Invalid token detected, clearing authentication:', error);
28+
localStorage.removeItem(config.tokenKey);
1929
store.dispatch(logoutUser());
20-
} else {
21-
httpService.setjwt(token);
22-
store.dispatch(setCurrentUser(decoded));
2330
}
2431
}

0 commit comments

Comments
 (0)