feat(5.20): Custom Fields — plugin-defined fields in Contact UI

- Add CustomFieldDefinition to PluginManifest (text/number/date/select/multiselect/boolean)
- Add GET/PATCH /api/v1/contacts/{id}/custom-fields routes
- Merge plugin definitions with stored values in contacts.custom JSONB
- Add CustomFieldRenderer component (read + edit modes)
- Integrate into ContactDetail (read-only) and ContactEditModal (editable)
- Add custom_fields to pluginStore and active manifests API
- Add useCustomFields/useUpdateCustomFields React Query hooks
- Tests: test_custom_fields.py (9 tests) + CustomFieldRenderer.test.tsx (5 tests)
- i18n keys already present (contacts.customFields)
This commit is contained in:
Agent Zero
2026-07-23 23:35:35 +02:00
parent 63b99ba489
commit bdad91a649
15 changed files with 762 additions and 4 deletions
+18
View File
@@ -138,6 +138,21 @@ class FrontendDashboardWidget(BaseModel):
permission: str = Field(default="", description="Optional permission required")
class CustomFieldDefinition(BaseModel):
"""A custom field definition contributed by a plugin manifest."""
name: str = Field(..., min_length=1, max_length=80, description="Unique field name (snake_case)")
label: str = Field(default="", description="Human-readable label (fallback if i18n key missing)")
label_key: str = Field(default="", description="i18n key for the field label")
field_type: str = Field(
..., pattern="^(text|number|date|select|multiselect|boolean)$", description="Field type"
)
options: list[str] = Field(default_factory=list, description="Options for select/multiselect types")
default_value: Any = Field(default=None, description="Default value for the field")
required: bool = Field(default=False, description="Whether the field is required")
entity: str = Field(default="contact", description="Entity type this field applies to (contact/file/etc)")
class MiniAppContribution(BaseModel):
"""A MiniApp contributed by a plugin manifest."""
@@ -214,6 +229,9 @@ class PluginManifest(BaseModel):
miniapps: list[MiniAppContribution] = Field(
default_factory=list, description="MiniApps contributed by this plugin"
)
custom_fields: list[CustomFieldDefinition] = Field(
default_factory=list, description="Custom field definitions contributed by this plugin"
)
@field_validator("name")
+1
View File
@@ -760,6 +760,7 @@ class PluginRegistry:
"detail_tabs": [tab.model_dump() for tab in m.detail_tabs],
"settings_pages": [page.model_dump() for page in m.settings_pages],
"dashboard_widgets": [widget.model_dump() for widget in m.dashboard_widgets],
"custom_fields": [cf.model_dump() for cf in m.custom_fields],
}
)
return manifests