diff --git a/ui/src/features/clips/ClipsPage.test.tsx b/ui/src/features/clips/ClipsPage.test.tsx index b2091272..b31b60ec 100644 --- a/ui/src/features/clips/ClipsPage.test.tsx +++ b/ui/src/features/clips/ClipsPage.test.tsx @@ -2,7 +2,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen, within } from '@testing-library/react' -import { MemoryRouter } from 'react-router-dom' +import userEvent from '@testing-library/user-event' +import { MemoryRouter, useLocation } from 'react-router-dom' import type { ClipListSnapshot } from '../../api/client' import type { CameraResponse } from '../../api/generated/types' @@ -60,10 +61,16 @@ function renderEventsPage({ render( + , ) } +function LocationProbe() { + const location = useLocation() + return

{`${location.pathname}${location.search}`}

+} + describe('ClipsPage event list', () => { afterEach(() => { cleanup() @@ -147,4 +154,33 @@ describe('ClipsPage event list', () => { expect(emptyState).toBeTruthy() expect(screen.queryByRole('table')).toBeNull() }) + + it('maps quick filters to existing URL-synced clip query params', async () => { + // Given: The Events page is open with existing URL-backed filter state + const user = userEvent.setup() + renderEventsPage({ + clipList: { + httpStatus: 200, + limit: 25, + next_cursor: null, + has_more: false, + clips: [], + }, + route: '/events?detected=any', + }) + + // When: Applying quick filters and opening the advanced sheet + await user.click(screen.getByRole('button', { name: 'Alerts' })) + await user.click(screen.getByRole('button', { name: 'Packages' })) + await user.click(screen.getByRole('button', { name: 'High risk' })) + await user.click(screen.getByRole('button', { name: 'More filters' })) + + // Then: Quick filters should use existing query params and full filters stay available + expect(screen.getByTestId('location').textContent).toContain('alerted=true') + expect(screen.getByTestId('location').textContent).toContain('activity_type=package') + expect(screen.getByTestId('location').textContent).toContain('risk_level=high') + expect(screen.getByLabelText('Alert status')).toBeTruthy() + expect(screen.getByLabelText('Detection')).toBeTruthy() + expect(screen.getByLabelText('Results per page')).toBeTruthy() + }) }) diff --git a/ui/src/features/clips/ClipsPage.tsx b/ui/src/features/clips/ClipsPage.tsx index 2d557ebf..8376a596 100644 --- a/ui/src/features/clips/ClipsPage.tsx +++ b/ui/src/features/clips/ClipsPage.tsx @@ -6,9 +6,9 @@ import { useCamerasQuery } from '../../api/hooks/useCamerasQuery' import { useClipsQuery } from '../../api/hooks/useClipsQuery' import { ApiKeyGate } from '../../components/ui/ApiKeyGate' import { Button } from '../../components/ui/Button' -import { Card } from '../../components/ui/Card' import { EmptyState } from '../../components/ui/EmptyState' import { EventCard } from '../../components/ui/EventCard' +import { FilterChips, type FilterChip } from '../../components/ui/FilterChips' import { MediaPanel } from '../../components/ui/MediaPanel' import { RiskBadge } from '../../components/ui/RiskBadge' import { StatusBadge } from '../../components/ui/StatusBadge' @@ -44,6 +44,7 @@ interface ClipsFilterPanelProps { cameraOptions: string[] initialFormState: ClipsFilterFormState onApply: (form: ClipsFilterFormState) => void + onClose: () => void onReset: () => void } @@ -51,6 +52,7 @@ function ClipsFilterPanel({ cameraOptions, initialFormState, onApply, + onClose, onReset, }: ClipsFilterPanelProps) { const [formState, setFormState] = useState(initialFormState) @@ -66,7 +68,14 @@ function ClipsFilterPanel({ } return ( - +
+
+
+

More filters

+

Use detailed filters when the quick chips are not enough.

+
+ +
@@ -173,7 +182,7 @@ function ClipsFilterPanel({