frontend_nyla/src/hooks/usePrompts.ts

493 lines
No EOL
17 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi';
import { getUserDataCache } from '../utils/userCache';
import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions';
import {
fetchPrompts as fetchPromptsApi,
fetchPromptById as fetchPromptByIdApi,
createPrompt as createPromptApi,
updatePrompt as updatePromptApi,
deletePrompt as deletePromptApi,
type Prompt,
type AttributeDefinition,
type PaginationParams
} from '../api/promptApi';
// Re-export types for backward compatibility
export type { Prompt, AttributeDefinition, PaginationParams };
// Re-export AttributeOption for backward compatibility
export interface AttributeOption {
value: string | number;
label: string | { [key: string]: string };
}
// Prompts list hook
export function usePrompts() {
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Prompt[]>();
const { checkPermission } = usePermissions();
// Fetch attributes from backend
const fetchAttributes = useCallback(async () => {
try {
const response = await api.get('/api/attributes/Prompt');
// Extract attributes from response - check if response.data.attributes exists, otherwise check if response.data is an array
let attrs: AttributeDefinition[] = [];
if (response.data?.attributes && Array.isArray(response.data.attributes)) {
attrs = response.data.attributes;
} else if (Array.isArray(response.data)) {
attrs = response.data;
} else if (response.data && typeof response.data === 'object') {
// Try to find any array property in the response
const keys = Object.keys(response.data);
for (const key of keys) {
if (Array.isArray(response.data[key])) {
attrs = response.data[key];
break;
}
}
}
setAttributes(attrs);
return attrs;
} catch (error: any) {
console.error('Error fetching attributes:', error);
setAttributes([]);
return [];
}
}, []);
// Fetch permissions from backend
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'Prompt');
setPermissions(perms);
return perms;
} catch (error: any) {
console.error('Error fetching permissions:', error);
const defaultPerms: UserPermissions = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
setPermissions(defaultPerms);
return defaultPerms;
}
}, [checkPermission]);
const fetchPrompts = useCallback(async (params?: PaginationParams) => {
try {
const data = await fetchPromptsApi(request, params);
// Handle paginated response
if (data && typeof data === 'object' && 'items' in data) {
const items = Array.isArray(data.items) ? data.items : [];
setPrompts(items);
if (data.pagination) {
setPagination(data.pagination);
}
} else {
// Handle non-paginated response (backward compatibility)
const items = Array.isArray(data) ? data : [];
setPrompts(items);
setPagination(null);
}
} catch (error: any) {
// Error is already handled by useApiRequest
setPrompts([]);
setPagination(null);
}
}, [request]);
// Optimistically remove a prompt from the local state
const removeOptimistically = (promptId: string) => {
setPrompts(prevPrompts => prevPrompts.filter(prompt => prompt.id !== promptId));
};
// Optimistically update a prompt in the local state
const updateOptimistically = (promptId: string, updateData: { name: string; content: string }) => {
setPrompts(prevPrompts =>
prevPrompts.map(prompt =>
prompt.id === promptId
? { ...prompt, ...updateData }
: prompt
)
);
};
// Fetch a single prompt by ID
const fetchPromptById = useCallback(async (promptId: string): Promise<Prompt | null> => {
return await fetchPromptByIdApi(request, promptId);
}, [request]);
// Generate edit fields from attributes dynamically
const generateEditFieldsFromAttributes = useCallback((): Array<{
key: string;
label: string;
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
editable?: boolean;
required?: boolean;
validator?: (value: any) => string | null;
minRows?: number;
maxRows?: number;
options?: Array<{ value: string | number; label: string }>;
optionsReference?: string; // For options that need to be fetched (e.g., "user.role")
}> => {
if (!attributes || attributes.length === 0) {
return [];
}
const editableFields = attributes
.filter(attr => {
// Filter out non-editable fields based on readonly/editable flags
if (attr.readonly === true || attr.editable === false) {
return false; // Don't show readonly fields in edit form
}
// Also filter out common non-editable fields
const nonEditableFields = ['id', 'mandateId', '_createdBy', '_hideDelete'];
return !nonEditableFields.includes(attr.name);
})
.map(attr => {
// Check if this is a content field first - check this BEFORE type mapping
const isContentField = attr.name === 'content' ||
attr.name.toLowerCase().includes('content') ||
attr.name.toLowerCase().includes('description');
// Map backend attribute type to form field type
// IMPORTANT: Check for content fields FIRST before other type checks
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
let optionsReference: string | undefined = undefined;
if (isContentField) {
// Force content fields to ALWAYS be textarea, regardless of attribute type
fieldType = 'textarea';
} else if (attr.type === 'checkbox') {
fieldType = 'boolean';
} else if (attr.type === 'email') {
fieldType = 'email';
} else if (attr.type === 'date') {
fieldType = 'date';
} else if (attr.type === 'select') {
fieldType = 'enum';
// Handle options - can be array or string reference
if (Array.isArray(attr.options)) {
options = attr.options.map(opt => {
const labelValue = typeof opt.label === 'string'
? opt.label
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') {
optionsReference = attr.options;
}
} else if (attr.type === 'multiselect') {
fieldType = 'multiselect';
// Handle options - can be array or string reference
if (Array.isArray(attr.options)) {
options = attr.options.map(opt => {
const labelValue = typeof opt.label === 'string'
? opt.label
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') {
optionsReference = attr.options;
}
} else if (attr.type === 'textarea') {
fieldType = 'textarea';
} else if (attr.type === 'text') {
// Check if it should be textarea based on name
if (attr.name.toLowerCase().includes('text') || attr.name.toLowerCase().includes('note')) {
fieldType = 'textarea';
} else {
fieldType = 'string';
}
}
// Legacy support for old format
else if (attr.type === 'boolean') {
fieldType = 'boolean';
} else if (attr.type === 'enum' && attr.filterOptions) {
fieldType = 'enum';
options = attr.filterOptions.map(opt => ({ value: opt, label: opt }));
}
// Define validators and required fields
let required = attr.required === true;
let validator: ((value: any) => string | null) | undefined = undefined;
let minRows: number | undefined = undefined;
let maxRows: number | undefined = undefined;
// Match create button configuration for prompts
if (attr.name === 'name') {
required = true;
validator = (value: string) => {
if (!value || value.trim() === '') {
return 'Prompt name cannot be empty';
}
if (value.length > 100) {
return 'Prompt name cannot exceed 100 characters';
}
return null;
};
} else if (isContentField || attr.name === 'content') {
required = true;
minRows = 6; // Match create button: minRows: 6
maxRows = 12; // Match create button: maxRows: 12
validator = (value: string) => {
if (!value || value.trim() === '') {
return 'Prompt content cannot be empty';
}
if (value.length > 10000) {
return 'Prompt content cannot exceed 10,000 characters';
}
return null;
};
} else if (fieldType === 'textarea') {
// Default textarea settings for other textarea fields
minRows = 4;
maxRows = 8;
}
// Multiselect validation
else if (fieldType === 'multiselect' && required) {
validator = (value: any[]) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return `${attr.label} is required`;
}
return null;
};
}
return {
key: attr.name,
label: attr.label || attr.name,
type: fieldType,
editable: attr.editable !== false && attr.readonly !== true,
required,
validator,
minRows,
maxRows,
options,
optionsReference
};
});
// Ensure we always return at least the basic fields if attributes exist
// This handles cases where all fields might be filtered out
if (editableFields.length === 0 && attributes.length > 0) {
// If all fields were filtered out, include all attributes as editable string fields
// Match create button configuration
return attributes.map(attr => {
const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content');
const fieldType = isContentField ? 'textarea' as const : 'string' as const;
let required = false;
let validator: ((value: any) => string | null) | undefined = undefined;
let minRows: number | undefined = undefined;
let maxRows: number | undefined = undefined;
if (attr.name === 'name') {
required = true;
validator = (value: string) => {
if (!value || value.trim() === '') {
return 'Prompt name cannot be empty';
}
if (value.length > 100) {
return 'Prompt name cannot exceed 100 characters';
}
return null;
};
} else if (isContentField) {
required = true;
minRows = 6; // Match create button
maxRows = 12; // Match create button
validator = (value: string) => {
if (!value || value.trim() === '') {
return 'Prompt content cannot be empty';
}
if (value.length > 10000) {
return 'Prompt content cannot exceed 10,000 characters';
}
return null;
};
}
return {
key: attr.name,
label: attr.label || attr.name,
type: fieldType,
editable: true,
required,
validator,
minRows,
maxRows
};
});
}
return editableFields;
}, [attributes]);
// Ensure attributes are loaded - can be called by EditActionButton
const ensureAttributesLoaded = useCallback(async () => {
// If attributes are already loaded, return them
if (attributes && attributes.length > 0) {
return attributes;
}
// Otherwise, fetch them and return the result
const fetchedAttributes = await fetchAttributes();
return fetchedAttributes;
}, [attributes, fetchAttributes]);
// Fetch attributes and permissions on mount
useEffect(() => {
fetchAttributes();
fetchPermissions();
}, [fetchAttributes, fetchPermissions]);
// Initial fetch
useEffect(() => {
fetchPrompts();
}, [fetchPrompts]);
return {
prompts,
loading,
error,
refetch: fetchPrompts,
removeOptimistically,
updateOptimistically,
attributes,
permissions,
pagination,
fetchPromptById,
generateEditFieldsFromAttributes,
ensureAttributesLoaded // Generic function to ensure attributes are loaded
};
}
// Prompt operations hook
export function usePromptOperations() {
const [deletingPrompts, setDeletingPrompts] = useState<Set<string>>(new Set());
const [creatingPrompt, setCreatingPrompt] = useState(false);
const { request, isLoading } = useApiRequest();
const [deleteError, setDeleteError] = useState<string | null>(null);
const [createError, setCreateError] = useState<string | null>(null);
const [updateError, setUpdateError] = useState<string | null>(null);
const handlePromptDelete = async (promptId: string) => {
setDeleteError(null);
setDeletingPrompts(prev => new Set(prev).add(promptId));
try {
await deletePromptApi(request, promptId);
// Add a small delay to ensure backend has time to process
await new Promise(resolve => setTimeout(resolve, 300));
return true;
} catch (error: any) {
setDeleteError(error.message);
return false;
} finally {
setDeletingPrompts(prev => {
const newSet = new Set(prev);
newSet.delete(promptId);
return newSet;
});
}
};
const handlePromptCreate = async (promptData: { name: string; content: string }) => {
setCreateError(null);
setCreatingPrompt(true);
try {
// Get mandateId from currentUser in sessionStorage cache
const currentUserData = getUserDataCache();
const mandateId = currentUserData?.mandateId || '';
// Structure the request body as required by the API
const requestBody = {
mandateId: mandateId,
name: promptData.name,
content: promptData.content
};
const newPrompt = await createPromptApi(request, requestBody);
return { success: true, promptData: newPrompt };
} catch (error: any) {
setCreateError(error.message);
return { success: false, error: error.message };
} finally {
setCreatingPrompt(false);
}
};
const handlePromptUpdate = async (promptId: string, updateData: { name: string; content: string }, originalData?: any) => {
setUpdateError(null);
try {
// Get mandateId from currentUser in sessionStorage cache
const currentUserData = getUserDataCache();
const mandateId = currentUserData?.mandateId || '';
// Structure the request body as required by the API
const requestBody = {
mandateId: mandateId,
name: updateData.name,
content: updateData.content
};
const updatedPrompt = await updatePromptApi(request, promptId, requestBody);
return { success: true, promptData: updatedPrompt };
} catch (error: any) {
const errorMessage = error.response?.data?.message || error.message || 'Failed to update prompt';
const statusCode = error.response?.status;
setUpdateError(errorMessage);
// Return detailed error information for proper handling
return {
success: false,
error: errorMessage,
statusCode,
isPermissionError: statusCode === 403,
isValidationError: statusCode === 400
};
}
};
return {
deletingPrompts,
creatingPrompt,
deleteError,
createError,
updateError,
handlePromptDelete,
handlePromptCreate,
handlePromptUpdate,
isLoading
};
}