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
@@ -14,6 +14,8 @@ import { usePluginStore } from '@/store/pluginStore';
import { useAIUIControlStore } from '@/store/aiUIControlStore';
import { PluginPage } from '@/components/plugins/PluginLoader';
import { useAuthStore } from '@/store/authStore';
import { CustomFieldRenderer } from '@/components/contacts/CustomFieldRenderer';
import { useCustomFields } from '@/api/customFields';
import {
type UnifiedContact,
type ContactPerson,
@@ -177,6 +179,10 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe
}, [aiActiveModal]);
const user = useAuthStore(s => s.user);
// Custom fields from plugin definitions
const customFieldDefs = usePluginStore(s => s.getCustomFieldsForEntity('contact'));
const { data: customFieldsData } = useCustomFields(contact?.id);
if (loading) {
return (
<div className="flex items-center justify-center py-12" data-testid="contact-detail-loading">
@@ -420,11 +426,12 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe
</Section>
{/* Custom Fields */}
{contact.custom && Object.keys(contact.custom).length > 0 && (
{contact.id && customFieldDefs.length > 0 && (
<Section title={t('contacts.customFields')}>
<pre className="text-xs text-secondary-700 bg-secondary-50 rounded p-2 overflow-x-auto">
{JSON.stringify(contact.custom, null, 2)}
</pre>
<CustomFieldRenderer
fields={customFieldsData?.fields || []}
mode="read"
/>
</Section>
)}