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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user