Skip to content

Commit 42fa8b4

Browse files
committed
#129 add X button to remove cases from search results with tests
1 parent 4d2ea44 commit 42fa8b4

4 files changed

Lines changed: 182 additions & 3 deletions

File tree

frontend/src/components/app/SearchResult.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import React from 'react';
22
import type { SearchResult as SearchResultType } from '../../../../shared/types';
33
import { parseDateString, formatDisplayDate } from '../../../../shared/DateTimeUtils';
44
import SearchStatus from './SearchStatus';
5-
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
5+
import { ArrowTopRightOnSquareIcon, XMarkIcon } from '@heroicons/react/24/outline';
66
import { PORTAL_CASE_URL } from '../../aws-exports';
7+
import { useRemoveCase } from '../../hooks/useCaseSearch';
8+
import { Button as HeadlessButton } from '@headlessui/react';
79

810
interface SearchResultProps {
911
searchResult: SearchResultType;
1012
}
1113

1214
const SearchResult: React.FC<SearchResultProps> = ({ searchResult: sr }) => {
15+
const removeCase = useRemoveCase();
1316
// Add a safety check to ensure we have a properly structured case object
1417
if (!sr?.zipCase?.caseNumber) {
1518
console.error('Invalid case object received by SearchResult:', sr);
@@ -18,8 +21,21 @@ const SearchResult: React.FC<SearchResultProps> = ({ searchResult: sr }) => {
1821

1922
const { zipCase: c, caseSummary: summary } = sr;
2023

24+
const handleRemove = () => {
25+
removeCase(c.caseNumber);
26+
};
27+
2128
return (
22-
<div className="bg-white rounded-lg shadow overflow-hidden border-t border-gray-100">
29+
<div className="bg-white rounded-lg shadow overflow-hidden border-t border-gray-100 relative group">
30+
{/* Remove button - appears in upper right corner */}
31+
<HeadlessButton
32+
onClick={handleRemove}
33+
className="absolute top-2 right-2 p-1.5 rounded text-gray-300 transition-colors duration-200 group-hover:text-gray-500 data-hover:text-gray-700 data-hover:bg-gray-100 data-focus:text-gray-700 data-focus:bg-gray-100 focus:outline-none data-focus:outline data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-gray-400"
34+
aria-label="Remove case from results"
35+
title="Remove case from results"
36+
>
37+
<XMarkIcon className="h-5 w-5" />
38+
</HeadlessButton>
2339
<div className="p-4 sm:p-6">
2440
<div className="flex items-start">
2541
<div className="flex-shrink-0 mr-4">

frontend/src/components/app/__tests__/SearchResult.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,14 @@ describe('SearchResult component', () => {
315315
expect(screen.getByText('Dept A')).toBeInTheDocument();
316316
expect(screen.getByText('Dept B')).toBeInTheDocument();
317317
});
318+
319+
it('renders remove button that appears on hover', () => {
320+
const testCase = createTestCase();
321+
render(<SearchResult searchResult={testCase} />, { wrapper: createWrapper() });
322+
323+
// Remove button should be present
324+
const removeButton = screen.getByRole('button', { name: /remove case from results/i });
325+
expect(removeButton).toBeInTheDocument();
326+
expect(removeButton).toHaveAttribute('title', 'Remove case from results');
327+
});
318328
});

frontend/src/hooks/__tests__/useCaseSearch.test.tsx

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
55
import { renderHook, act, waitFor } from '@testing-library/react';
66
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
77
import React from 'react';
8-
import { useSearchResults, useConsolidatedPolling } from '../useCaseSearch';
8+
import { useSearchResults, useConsolidatedPolling, useRemoveCase } from '../useCaseSearch';
99

1010
vi.mock('../../aws-exports', () => ({
1111
API_URL: 'http://test-api.example.com',
@@ -167,3 +167,129 @@ describe('useConsolidatedPolling - state management', () => {
167167
});
168168
});
169169
});
170+
171+
describe('useRemoveCase', () => {
172+
beforeEach(() => {
173+
vi.resetAllMocks();
174+
});
175+
176+
afterEach(() => {
177+
vi.resetAllMocks();
178+
});
179+
180+
it('should remove a case from results and batches', () => {
181+
const testData = {
182+
results: {
183+
case123: {
184+
zipCase: {
185+
caseNumber: 'case123',
186+
fetchStatus: { status: 'complete' },
187+
},
188+
},
189+
case456: {
190+
zipCase: {
191+
caseNumber: 'case456',
192+
fetchStatus: { status: 'complete' },
193+
},
194+
},
195+
},
196+
searchBatches: [['case123', 'case456']],
197+
};
198+
199+
const queryClient = createTestQueryClient();
200+
queryClient.setQueryData(['searchResults'], testData);
201+
const wrapper = createWrapper(queryClient);
202+
203+
const { result } = renderHook(() => useRemoveCase(), { wrapper });
204+
205+
// Remove case123
206+
act(() => {
207+
result.current('case123');
208+
});
209+
210+
// Check that the case was removed
211+
const updatedData = queryClient.getQueryData(['searchResults']);
212+
expect(updatedData).toEqual({
213+
results: {
214+
case456: {
215+
zipCase: {
216+
caseNumber: 'case456',
217+
fetchStatus: { status: 'complete' },
218+
},
219+
},
220+
},
221+
searchBatches: [['case456']],
222+
});
223+
});
224+
225+
it('should remove empty batches after removing all cases', () => {
226+
const testData = {
227+
results: {
228+
case123: {
229+
zipCase: {
230+
caseNumber: 'case123',
231+
fetchStatus: { status: 'complete' },
232+
},
233+
},
234+
},
235+
searchBatches: [['case123']],
236+
};
237+
238+
const queryClient = createTestQueryClient();
239+
queryClient.setQueryData(['searchResults'], testData);
240+
const wrapper = createWrapper(queryClient);
241+
242+
const { result } = renderHook(() => useRemoveCase(), { wrapper });
243+
244+
// Remove case123
245+
act(() => {
246+
result.current('case123');
247+
});
248+
249+
// Check that the batch was removed too
250+
const updatedData = queryClient.getQueryData(['searchResults']);
251+
expect(updatedData).toEqual({
252+
results: {},
253+
searchBatches: [],
254+
});
255+
});
256+
257+
it('should handle removing a non-existent case gracefully', () => {
258+
const testData = {
259+
results: {
260+
case123: {
261+
zipCase: {
262+
caseNumber: 'case123',
263+
fetchStatus: { status: 'complete' },
264+
},
265+
},
266+
},
267+
searchBatches: [['case123']],
268+
};
269+
270+
const queryClient = createTestQueryClient();
271+
queryClient.setQueryData(['searchResults'], testData);
272+
const wrapper = createWrapper(queryClient);
273+
274+
const { result } = renderHook(() => useRemoveCase(), { wrapper });
275+
276+
// Remove non-existent case
277+
act(() => {
278+
result.current('case999');
279+
});
280+
281+
// Check that the state remains the same
282+
const updatedData = queryClient.getQueryData(['searchResults']);
283+
expect(updatedData).toEqual({
284+
results: {
285+
case123: {
286+
zipCase: {
287+
caseNumber: 'case123',
288+
fetchStatus: { status: 'complete' },
289+
},
290+
},
291+
},
292+
searchBatches: [['case123']],
293+
});
294+
});
295+
});

frontend/src/hooks/useCaseSearch.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,33 @@ export function useSearchResults() {
9292
});
9393
}
9494

95+
export function useRemoveCase() {
96+
const queryClient = useQueryClient();
97+
98+
return (caseNumber: string) => {
99+
const currentState = queryClient.getQueryData<ResultsState>(['searchResults']);
100+
101+
if (!currentState) {
102+
return;
103+
}
104+
105+
// Remove the case from results
106+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
107+
const { [caseNumber]: _removed, ...remainingResults } = currentState.results;
108+
109+
// Remove the case from all batches
110+
const updatedBatches = currentState.searchBatches
111+
.map(batch => batch.filter(cn => cn !== caseNumber))
112+
.filter(batch => batch.length > 0); // Remove empty batches
113+
114+
// Update the query cache
115+
queryClient.setQueryData(['searchResults'], {
116+
results: remainingResults,
117+
searchBatches: updatedBatches,
118+
});
119+
};
120+
}
121+
95122
export function useConsolidatedPolling() {
96123
const queryClient = useQueryClient();
97124

0 commit comments

Comments
 (0)