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:
Agent Zero
2026-07-26 03:02:25 +02:00
parent 444c7fdb88
commit a7e3890634
22 changed files with 2684 additions and 8 deletions
+109
View File
@@ -0,0 +1,109 @@
/**
* Custom field definitions API hooks — CRUD for entity-level field definitions.
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPatch, apiDelete } from './client';
export type CustomFieldType = 'text' | 'number' | 'date' | 'select' | 'multiselect' | 'boolean';
export type CustomFieldEntity = 'contact' | 'company';
export interface CustomFieldDefinition {
id: string;
tenant_id: string;
entity: string;
name: string;
label: string;
field_type: CustomFieldType;
options: string[];
default_value: any;
required: boolean;
is_active: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
export interface CustomFieldDefinitionCreate {
entity: string;
name: string;
label: string;
field_type: CustomFieldType;
options?: string[];
default_value?: any;
required?: boolean;
is_active?: boolean;
sort_order?: number;
}
export interface CustomFieldDefinitionUpdate {
name?: string;
label?: string;
field_type?: CustomFieldType;
options?: string[];
default_value?: any;
required?: boolean;
is_active?: boolean;
sort_order?: number;
}
export interface CustomFieldDefinitionsResponse {
items: CustomFieldDefinition[];
total: number;
}
/**
* Fetch custom field definitions, optionally filtered by entity.
*/
export function useCustomFieldDefinitions(entity?: string) {
return useQuery({
queryKey: ['custom-field-definitions', entity ?? 'all'],
queryFn: () => {
const params = entity ? `?entity=${encodeURIComponent(entity)}` : '';
return apiGet<CustomFieldDefinitionsResponse>(`/custom-fields/definitions${params}`);
},
});
}
/**
* Create a new custom field definition.
*/
export function useCreateCustomFieldDefinition() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CustomFieldDefinitionCreate) =>
apiPost<CustomFieldDefinition>('/custom-fields/definitions', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['custom-field-definitions'] });
},
});
}
/**
* Update an existing custom field definition.
*/
export function useUpdateCustomFieldDefinition() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: CustomFieldDefinitionUpdate }) =>
apiPatch<CustomFieldDefinition>(`/custom-fields/definitions/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['custom-field-definitions'] });
},
});
}
/**
* Delete a custom field definition.
*/
export function useDeleteCustomFieldDefinition() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiDelete<void>(`/custom-fields/definitions/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['custom-field-definitions'] });
},
});
}
+2 -1
View File
@@ -43,13 +43,14 @@ export function useNotifications() {
});
}
export function useUnreadNotificationCount() {
export function useUnreadNotificationCount(options?: { refetchInterval?: number }) {
return useQuery({
queryKey: ['notifications', 'unread-count'],
queryFn: async () => {
const data = await apiGet<UnreadCountResponse>('/notifications/unread-count');
return data.count;
},
refetchInterval: options?.refetchInterval,
});
}