import React, { useState } from 'react'; import { MdModeEdit } from 'react-icons/md'; import { useLanguage } from '../../../../providers/language/LanguageContext'; import { Popup } from '../../../UiComponents/Popup'; import { FormGeneratorForm, AttributeDefinition } from '../../FormGeneratorForm'; import styles from '../ActionButton.module.css'; export interface EditActionButtonProps { row: T; onEdit?: (row: T) => void; disabled?: boolean | { disabled: boolean; message?: string }; loading?: boolean; className?: string; title?: string; isEditing?: boolean; hookData: any; // REQUIRED: Contains all hook data including operations // Field mappings idField?: string; // Field name for the unique identifier nameField?: string; // Field name for display name typeField?: string; // Field name for type/mime type operationName?: string; // Name of the operation function in hookData loadingStateName?: string; // Name of the loading state in hookData // Function name in hookData to fetch a single item (e.g., 'fetchPromptById', 'fetchItem') fetchItemFunctionName?: string; // Entity type for FormGeneratorForm (e.g., "Prompt", "User", "FileItem") entityType?: string; // Optional: Pre-fetched attributes (if available in hookData) attributes?: any[]; } export function EditActionButton({ row, onEdit, disabled = false, loading = false, className = '', title, isEditing = false, hookData, idField = 'id', operationName = 'handleFileUpdate', loadingStateName = 'editingFiles', fetchItemFunctionName = 'fetchPromptById', entityType, attributes: providedAttributes }: EditActionButtonProps) { const { t } = useLanguage(); const [internalLoading, setInternalLoading] = useState(false); const [isPopupOpen, setIsPopupOpen] = useState(false); const [editData, setEditData] = useState(null); const [fetchingData, setFetchingData] = useState(false); // Extract disabled state and tooltip message const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false; const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined; // Validate that hookData is provided if (!hookData) { throw new Error('EditActionButton requires hookData to be provided'); } // Get entity type from hookData or props const getEntityType = (): string | undefined => { if (entityType) return entityType; if (hookData.entityType) return hookData.entityType; if (hookData.entityName) return hookData.entityName; // Try to infer from hookData attributes if available if (hookData.attributes && Array.isArray(hookData.attributes) && hookData.attributes.length > 0) { // Could potentially infer from attribute structure, but safer to require explicit entityType return undefined; } return undefined; }; // Get attributes from hookData or props const getAttributes = () => { if (providedAttributes) return providedAttributes; if (hookData.attributes && Array.isArray(hookData.attributes)) return hookData.attributes; return undefined; }; const handleClick = async (e: React.MouseEvent) => { e.stopPropagation(); if (!isDisabled && !loading && !isEditing && !internalLoading && !fetchingData && !isPopupOpen) { // If onEdit callback is provided, call it and return early (custom handling) // The page will handle opening its own modal/form if (onEdit) { setInternalLoading(true); try { await onEdit(row); } finally { setInternalLoading(false); } return; // Don't open the built-in popup when custom onEdit is provided } // Otherwise, use the built-in popup form setInternalLoading(true); setFetchingData(true); try { const itemId = (row as any)[idField]; // Fetch current item data - use generic fetch function from hookData let freshData: T | null = null; if (itemId) { const possibleFunctionNames = [ fetchItemFunctionName, 'fetchItemById', 'fetchItem', 'getItemById', 'getItem' ].filter(Boolean); let fetchFunction: ((id: string) => Promise) | null = null; for (const funcName of possibleFunctionNames) { if (hookData[funcName] && typeof hookData[funcName] === 'function') { fetchFunction = hookData[funcName]; break; } } if (fetchFunction) { try { freshData = await fetchFunction(itemId); } catch (error: any) { console.error('Failed to fetch fresh data:', error); } } } // Ensure attributes are loaded - use generic function from hookData if available if (hookData.ensureAttributesLoaded && typeof hookData.ensureAttributesLoaded === 'function') { await hookData.ensureAttributesLoaded(); } // Use fresh data if available, otherwise use row data setEditData(freshData || row); // Set fetchingData to false first setFetchingData(false); // Wait for React to update state await new Promise(resolve => setTimeout(resolve, 0)); // Open popup AFTER data is ready - like CreateButton (no loading state shown) setIsPopupOpen(true); } finally { setInternalLoading(false); } } }; const handleSave = async (updatedData: T) => { if (!editData) return; try { setInternalLoading(true); // Get the item ID from the row const itemId = (editData as any)[idField]; // Get edit fields configuration from attributes const attributes = getAttributes(); const fields = attributes || []; // Extract the fields to update from the edit data const updateData: any = {}; fields.forEach((field: AttributeDefinition) => { if (field.editable !== false) { const fieldName = field.name; const value = (updatedData as any)[fieldName]; if (value !== undefined) { updateData[fieldName] = value; } } }); // Check if optimistic update is available const updateOptimistically = hookData.updateOptimistically; // Validate required operation exists if (!hookData[operationName]) { throw new Error(`EditActionButton requires hookData.${operationName} to be defined`); } // Optimistically update the UI immediately if (updateOptimistically) { updateOptimistically(itemId, updateData); } // Use hookData operation to update in the background const result = await hookData[operationName](itemId, updateData, editData); const success = result?.success || result === true; if (success) { // Close popup and reset state on success setIsPopupOpen(false); setEditData(null); // If we used optimistic update, refetch to get fresh data from backend // This ensures we have the latest data including any server-side transformations if (hookData.refetch) { await hookData.refetch(); } } else { // If update failed, revert optimistic update if (updateOptimistically && hookData.refetch) { // Revert by refetching original data await hookData.refetch(); } // Close popup on error setIsPopupOpen(false); setEditData(null); } } catch (error: any) { // If update failed, revert optimistic update if (hookData.updateOptimistically && hookData.refetch) { await hookData.refetch(); } console.error('Failed to update item:', error); setIsPopupOpen(false); setEditData(null); } finally { setInternalLoading(false); } }; const handleCancel = () => { setIsPopupOpen(false); setEditData(null); }; const buttonTitle = title || t('Bearbeiten'); // Use hookData editing state if available, otherwise use passed isEditing const loadingState = hookData?.[loadingStateName]; const actualIsEditing = loadingState?.has((row as any)[idField]) || isEditing; const isLoading = loading || actualIsEditing || internalLoading || fetchingData; // Determine the final button title (tooltip) const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle; return ( <> {/* Edit Popup - Identical structure to CreateButton */} {editData && (() => { const entityTypeValue = getEntityType(); const attributesValue = getAttributes(); if (!entityTypeValue && !attributesValue) { console.warn('EditActionButton: entityType or attributes must be provided for FormGeneratorForm'); return (
{t('Fehler')}
); } return ( ); })()}
); } export default EditActionButton;