Phase 2: Tags UI, Custom Fields UI, Notifications Bell
- Tags UI: TagsPage (CRUD, color picker), TagBadge, TagSelector (multi-select, inline creation) - Custom Fields Backend: model, schema, service, routes, migration 0041 - Custom Fields Frontend: CustomFieldsPage (definitions CRUD), CustomFieldRenderer (dynamic field rendering) - Custom Fields: _collect_custom_field_definitions() extended to merge DB definitions with plugin definitions - Notifications Bell: NotificationBell (30s polling, unread badge), NotificationDropdown, NotificationItem - NotificationBell integrated into TopBar - Routes: /tags, /settings/custom-fields registered - Settings nav: Custom Fields entry added - Menu items: Tags added to automation plugin manifest
This commit is contained in:
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.db import get_db
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.models.contact import Contact
|
||||
from app.models.custom_field_definition import CustomFieldDefinition
|
||||
from app.plugins.registry import get_registry
|
||||
|
||||
router = APIRouter(prefix="/api/v1/contacts", tags=["custom-fields"])
|
||||
@@ -24,10 +25,19 @@ class CustomFieldUpdateRequest(BaseModel):
|
||||
values: dict[str, Any] = {}
|
||||
|
||||
|
||||
def _collect_custom_field_definitions(entity: str = "contact") -> list[dict[str, Any]]:
|
||||
"""Collect all custom field definitions from active plugin manifests."""
|
||||
async def _collect_custom_field_definitions(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
entity: str = "contact",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Collect all custom field definitions from plugin manifests and DB.
|
||||
|
||||
DB-stored definitions override plugin definitions with the same name.
|
||||
"""
|
||||
definitions: list[dict[str, Any]] = []
|
||||
seen_names: set[str] = set()
|
||||
|
||||
# 1. Collect from active plugin manifests
|
||||
registry = get_registry()
|
||||
for plugin in registry._plugins.values():
|
||||
manifest = plugin.manifest
|
||||
@@ -50,10 +60,40 @@ def _collect_custom_field_definitions(entity: str = "contact") -> list[dict[str,
|
||||
"plugin": manifest.name,
|
||||
}
|
||||
)
|
||||
|
||||
# 2. Collect from DB (user-defined custom field definitions)
|
||||
stmt = select(CustomFieldDefinition).where(
|
||||
CustomFieldDefinition.tenant_id == tenant_id,
|
||||
CustomFieldDefinition.entity == entity,
|
||||
CustomFieldDefinition.is_active == True, # noqa: E712
|
||||
).order_by(CustomFieldDefinition.sort_order, CustomFieldDefinition.name)
|
||||
result = await db.execute(stmt)
|
||||
db_definitions = result.scalars().all()
|
||||
|
||||
for d in db_definitions:
|
||||
if d.name in seen_names:
|
||||
# DB definition overrides plugin definition — replace it
|
||||
definitions = [x for x in definitions if x["name"] != d.name]
|
||||
else:
|
||||
seen_names.add(d.name)
|
||||
definitions.append(
|
||||
{
|
||||
"name": d.name,
|
||||
"label": d.label,
|
||||
"label_key": "",
|
||||
"field_type": d.field_type,
|
||||
"options": d.options or [],
|
||||
"default_value": d.default_value,
|
||||
"required": d.required,
|
||||
"entity": d.entity,
|
||||
"plugin": "user_defined",
|
||||
}
|
||||
)
|
||||
|
||||
return definitions
|
||||
|
||||
|
||||
def _merge_definitions_with_values(
|
||||
async def _merge_definitions_with_values(
|
||||
definitions: list[dict[str, Any]], stored: dict[str, Any] | None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Merge field definitions with stored values, applying defaults."""
|
||||
@@ -87,8 +127,8 @@ async def get_custom_fields(
|
||||
if contact is None:
|
||||
raise HTTPException(404, detail={"detail": "Contact not found", "code": "not_found"})
|
||||
|
||||
definitions = _collect_custom_field_definitions("contact")
|
||||
merged = _merge_definitions_with_values(definitions, contact.custom)
|
||||
definitions = await _collect_custom_field_definitions(db, tenant_id, "contact")
|
||||
merged = await _merge_definitions_with_values(definitions, contact.custom)
|
||||
return {"fields": merged}
|
||||
|
||||
|
||||
@@ -114,7 +154,7 @@ async def update_custom_fields(
|
||||
raise HTTPException(404, detail={"detail": "Contact not found", "code": "not_found"})
|
||||
|
||||
# Validate against definitions
|
||||
definitions = _collect_custom_field_definitions("contact")
|
||||
definitions = await _collect_custom_field_definitions(db, tenant_id, "contact")
|
||||
def_map = {d["name"]: d for d in definitions}
|
||||
|
||||
current_custom = dict(contact.custom or {})
|
||||
@@ -154,5 +194,5 @@ async def update_custom_fields(
|
||||
|
||||
contact.custom = current_custom
|
||||
await db.flush()
|
||||
merged = _merge_definitions_with_values(definitions, contact.custom)
|
||||
merged = await _merge_definitions_with_values(definitions, contact.custom)
|
||||
return {"fields": merged}
|
||||
|
||||
Reference in New Issue
Block a user