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:
@@ -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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user