299 lines
10 KiB
TypeScript
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;
|