feat(5.22): Saved Searches / Smart Lists — reusable filters for list views
- SavedFilter model with TenantMixin (name, entity_type, filter_criteria JSONB, user_id)
- Routes: GET/POST /saved-filters, DELETE /saved-filters/{id} with RBAC
- Alembic migration 0029_saved_filters creates saved_filters table
- Frontend: SavedFilters.tsx component with save/load/delete UI
- Frontend: api/savedFilters.ts with React Query hooks
- Integrated into ContactsListPage as example
- i18n keys for savedFilters.* in de.json and en.json
- Tests: test_saved_filters.py (9 tests) + SavedFilters.test.tsx (3 tests)
- Registered SavedFilter in conftest.py
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* SavedFilters component tests.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { SavedFilters } from '@/components/SavedFilters';
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
// Mock toast
|
||||
vi.mock('@/components/ui/Toast', () => ({
|
||||
useToast: () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock UI components
|
||||
vi.mock('@/components/ui/Input', () => ({
|
||||
Input: ({ value, onChange, label, ...props }: any) => (
|
||||
<div>
|
||||
{label && <label>{label}</label>}
|
||||
<input
|
||||
data-testid={props['data-testid'] || 'input'}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/Modal', () => ({
|
||||
Modal: ({ open, onClose, title, children }: any) =>
|
||||
open ? (
|
||||
<div data-testid="modal" role="dialog">
|
||||
<h2>{title}</h2>
|
||||
<button onClick={onClose}>Close</button>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/Button', () => ({
|
||||
Button: ({ children, onClick, disabled, ...props }: any) => (
|
||||
<button onClick={onClick} disabled={disabled} data-testid={props['data-testid'] || 'button'}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock saved filters API
|
||||
vi.mock('@/api/savedFilters', () => ({
|
||||
useSavedFilters: vi.fn(() => ({
|
||||
data: [
|
||||
{
|
||||
id: 'filter-1',
|
||||
name: 'Important Clients',
|
||||
entity_type: 'contacts',
|
||||
filter_criteria: { type: 'company' },
|
||||
user_id: 'user-1',
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
})),
|
||||
useCreateSavedFilter: vi.fn(() => ({ mutateAsync: vi.fn(), isPending: false })),
|
||||
useDeleteSavedFilter: vi.fn(() => ({ mutateAsync: vi.fn() })),
|
||||
}));
|
||||
|
||||
describe('SavedFilters', () => {
|
||||
it('renders saved filter buttons', () => {
|
||||
render(
|
||||
<SavedFilters
|
||||
entityType="contacts"
|
||||
currentCriteria={{}}
|
||||
onLoadFilter={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('saved-filters')).toBeInTheDocument();
|
||||
expect(screen.getByText('Important Clients')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens save modal when save button is clicked', () => {
|
||||
render(
|
||||
<SavedFilters
|
||||
entityType="contacts"
|
||||
currentCriteria={{}}
|
||||
onLoadFilter={vi.fn()}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('save-filter-btn'));
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('filter-name-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onLoadFilter when a saved filter is clicked', () => {
|
||||
const onLoadFilter = vi.fn();
|
||||
render(
|
||||
<SavedFilters
|
||||
entityType="contacts"
|
||||
currentCriteria={{}}
|
||||
onLoadFilter={onLoadFilter}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByText('Important Clients'));
|
||||
expect(onLoadFilter).toHaveBeenCalledWith({ type: 'company' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user