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:
Agent Zero
2026-07-23 23:44:50 +02:00
parent 2c9e74776e
commit 182af355d1
12 changed files with 663 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
"""SavedFilter model — tenant-scoped, user-scoped saved filter criteria."""
from __future__ import annotations
import uuid
from typing import Any
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class SavedFilter(Base, TenantMixin):
"""Saved filter — reusable filter criteria for list views (contacts, mail, calendar, DMS)."""
__tablename__ = "saved_filters"
__table_args__ = (
UniqueConstraint("tenant_id", "user_id", "entity_type", "name", name="uq_saved_filters_tenant_user_entity_name"),
Index("ix_saved_filters_tenant_user", "tenant_id", "user_id"),
Index("ix_saved_filters_tenant_entity", "tenant_id", "entity_type"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
entity_type: Mapped[str] = mapped_column(
String(50), nullable=False
) # contacts, mail, calendar, dms
filter_criteria: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
user_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)