frontend_nyla/src/components/FormGenerator/ActionButtons/EditActionButton/EditActionButton.tsx
2026-04-08 20:28:44 +02:00

299 lines
10 KiB
TypeScript

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<T = any> {
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<T = any>({
row,
onEdit,
disabled = false,
loading = false,
className = '',
title,
isEditing = false,
hookData,
idField = 'id',
operationName = 'handleFileUpdate',
loadingStateName = 'editingFiles',
fetchItemFunctionName = 'fetchPromptById',
entityType,
attributes: providedAttributes
}: EditActionButtonProps<T>) {
const { t } = useLanguage();
const [internalLoading, setInternalLoading] = useState(false);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [editData, setEditData] = useState<T | null>(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<T | null>) | 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 (
<>
<button
onClick={handleClick}
className={`${styles.actionButton} ${styles.edit} ${isLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
title={finalTitle}
disabled={isDisabled || isLoading}
>
<span className={styles.actionIcon}>
{isLoading ? '⏳' : <MdModeEdit />}
</span>
</button>
{/* Edit Popup - Identical structure to CreateButton */}
<Popup
isOpen={isPopupOpen}
title={t('Datei bearbeiten')}
onClose={handleCancel}
size="medium"
closable={!internalLoading}
>
{editData && (() => {
const entityTypeValue = getEntityType();
const attributesValue = getAttributes();
if (!entityTypeValue && !attributesValue) {
console.warn('EditActionButton: entityType or attributes must be provided for FormGeneratorForm');
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
{t('Fehler')}
</div>
);
}
return (
<FormGeneratorForm
entityType={entityTypeValue}
attributes={attributesValue}
data={editData}
mode="edit"
onSubmit={handleSave}
onCancel={handleCancel}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
);
})()}
</Popup>
</>
);
}
export default EditActionButton;