Improve the frontend 2
This commit is contained in:
485
frontend/src/components/ui/BaseDeleteModal/BaseDeleteModal.tsx
Normal file
485
frontend/src/components/ui/BaseDeleteModal/BaseDeleteModal.tsx
Normal file
@@ -0,0 +1,485 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Trash2, AlertTriangle, Info } from 'lucide-react';
|
||||
import { Modal, Button } from '../index';
|
||||
|
||||
export type DeleteMode = 'soft' | 'hard';
|
||||
|
||||
export interface EntityDisplayInfo {
|
||||
primaryText: string;
|
||||
secondaryText?: string;
|
||||
}
|
||||
|
||||
export interface DeleteModeOption {
|
||||
mode: DeleteMode;
|
||||
title: string;
|
||||
description: string;
|
||||
benefits: string;
|
||||
enabled: boolean;
|
||||
disabledMessage?: string;
|
||||
}
|
||||
|
||||
export interface DeleteWarning {
|
||||
title: string;
|
||||
items: string[];
|
||||
footer?: string;
|
||||
}
|
||||
|
||||
export interface DeletionSummaryData {
|
||||
[key: string]: string | number;
|
||||
}
|
||||
|
||||
export interface BaseDeleteModalProps<TEntity, TSummary = DeletionSummaryData> {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
entity: TEntity | null;
|
||||
onSoftDelete: (entityId: string) => Promise<void | TSummary>;
|
||||
onHardDelete?: (entityId: string) => Promise<void | TSummary>;
|
||||
isLoading?: boolean;
|
||||
|
||||
// Configuration
|
||||
title: string;
|
||||
getEntityId: (entity: TEntity) => string;
|
||||
getEntityDisplay: (entity: TEntity) => EntityDisplayInfo;
|
||||
|
||||
// Mode configuration
|
||||
softDeleteOption: Omit<DeleteModeOption, 'mode' | 'enabled'>;
|
||||
hardDeleteOption?: Omit<DeleteModeOption, 'mode'>;
|
||||
|
||||
// Warnings
|
||||
softDeleteWarning: DeleteWarning;
|
||||
hardDeleteWarning: DeleteWarning;
|
||||
|
||||
// Optional features
|
||||
requireConfirmText?: boolean;
|
||||
confirmText?: string;
|
||||
showSuccessScreen?: boolean;
|
||||
successTitle?: string;
|
||||
getSuccessMessage?: (entity: TEntity, mode: DeleteMode) => string;
|
||||
|
||||
// Deletion summary
|
||||
showDeletionSummary?: boolean;
|
||||
formatDeletionSummary?: (summary: TSummary) => DeletionSummaryData;
|
||||
deletionSummaryTitle?: string;
|
||||
|
||||
// Dependency checking (for hard delete)
|
||||
dependencyCheck?: {
|
||||
isLoading: boolean;
|
||||
canDelete: boolean;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
// Auto-close timing
|
||||
autoCloseDelay?: number;
|
||||
}
|
||||
|
||||
export function BaseDeleteModal<TEntity, TSummary = DeletionSummaryData>({
|
||||
isOpen,
|
||||
onClose,
|
||||
entity,
|
||||
onSoftDelete,
|
||||
onHardDelete,
|
||||
isLoading = false,
|
||||
title,
|
||||
getEntityId,
|
||||
getEntityDisplay,
|
||||
softDeleteOption,
|
||||
hardDeleteOption,
|
||||
softDeleteWarning,
|
||||
hardDeleteWarning,
|
||||
requireConfirmText = true,
|
||||
confirmText: customConfirmText = 'ELIMINAR',
|
||||
showSuccessScreen = false,
|
||||
successTitle,
|
||||
getSuccessMessage,
|
||||
showDeletionSummary = false,
|
||||
formatDeletionSummary,
|
||||
deletionSummaryTitle,
|
||||
dependencyCheck,
|
||||
autoCloseDelay,
|
||||
}: BaseDeleteModalProps<TEntity, TSummary>) {
|
||||
const [selectedMode, setSelectedMode] = useState<DeleteMode>('soft');
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState('');
|
||||
const [deletionResult, setDeletionResult] = useState<TSummary | null>(null);
|
||||
const [deletionComplete, setDeletionComplete] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Reset state when modal closes
|
||||
setShowConfirmation(false);
|
||||
setSelectedMode('soft');
|
||||
setConfirmText('');
|
||||
setDeletionResult(null);
|
||||
setDeletionComplete(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!entity) return null;
|
||||
|
||||
const entityDisplay = getEntityDisplay(entity);
|
||||
const entityId = getEntityId(entity);
|
||||
const isHardDeleteEnabled = hardDeleteOption?.enabled !== false;
|
||||
|
||||
const handleDeleteModeSelect = (mode: DeleteMode) => {
|
||||
setSelectedMode(mode);
|
||||
setShowConfirmation(true);
|
||||
setConfirmText('');
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
try {
|
||||
if (selectedMode === 'hard' && onHardDelete) {
|
||||
const result = await onHardDelete(entityId);
|
||||
if (result && showDeletionSummary) {
|
||||
setDeletionResult(result as TSummary);
|
||||
}
|
||||
} else {
|
||||
const result = await onSoftDelete(entityId);
|
||||
if (result && showDeletionSummary) {
|
||||
setDeletionResult(result as TSummary);
|
||||
}
|
||||
}
|
||||
|
||||
if (showSuccessScreen) {
|
||||
setDeletionComplete(true);
|
||||
if (autoCloseDelay) {
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, autoCloseDelay);
|
||||
}
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting entity:', error);
|
||||
// Error handling is done by parent component
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setShowConfirmation(false);
|
||||
setSelectedMode('soft');
|
||||
setConfirmText('');
|
||||
setDeletionResult(null);
|
||||
setDeletionComplete(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isConfirmDisabled =
|
||||
selectedMode === 'hard' &&
|
||||
requireConfirmText &&
|
||||
confirmText.toUpperCase() !== customConfirmText.toUpperCase();
|
||||
|
||||
// Show deletion result/summary
|
||||
if (deletionResult && showDeletionSummary && formatDeletionSummary) {
|
||||
const formattedSummary = formatDeletionSummary(deletionResult);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} size="md">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{deletionSummaryTitle || 'Eliminación Completada'}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{entityDisplay.primaryText} ha sido eliminado
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--background-secondary)] rounded-lg p-4 mb-6">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-3">Resumen de eliminación:</h4>
|
||||
<div className="space-y-2 text-sm text-[var(--text-secondary)]">
|
||||
{Object.entries(formattedSummary).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between">
|
||||
<span>{key}:</span>
|
||||
<span className="font-medium">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button variant="primary" onClick={handleClose}>
|
||||
Entendido
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// Show deletion success
|
||||
if (deletionComplete && showSuccessScreen) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} size="md">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{successTitle || (selectedMode === 'hard' ? 'Eliminado' : 'Desactivado')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{getSuccessMessage?.(entity, selectedMode) || `${entityDisplay.primaryText} procesado correctamente`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// Show confirmation step
|
||||
if (showConfirmation) {
|
||||
const isHardDelete = selectedMode === 'hard';
|
||||
const canDelete = !isHardDelete || !dependencyCheck || dependencyCheck.canDelete !== false;
|
||||
const warning = isHardDelete ? hardDeleteWarning : softDeleteWarning;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} size="md">
|
||||
<div className="p-6">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
{isHardDelete ? (
|
||||
<AlertTriangle className="w-8 h-8 text-red-500" />
|
||||
) : (
|
||||
<Info className="w-8 h-8 text-orange-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
{isHardDelete ? 'Confirmación de Eliminación Permanente' : 'Confirmación de Desactivación'}
|
||||
</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="bg-[var(--background-secondary)] p-3 rounded-lg mb-4">
|
||||
<p className="font-medium text-[var(--text-primary)]">{entityDisplay.primaryText}</p>
|
||||
{entityDisplay.secondaryText && (
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{entityDisplay.secondaryText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isHardDelete && dependencyCheck?.isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--text-primary)]"></div>
|
||||
<p className="mt-2 text-sm text-[var(--text-secondary)]">
|
||||
Verificando dependencias...
|
||||
</p>
|
||||
</div>
|
||||
) : isHardDelete && !canDelete ? (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4">
|
||||
<p className="font-medium text-red-700 dark:text-red-400 mb-2">
|
||||
⚠️ No se puede eliminar este elemento
|
||||
</p>
|
||||
<ul className="text-sm space-y-1 text-red-600 dark:text-red-300">
|
||||
{dependencyCheck?.warnings.map((warn, idx) => (
|
||||
<li key={idx}>• {warn}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className={isHardDelete ? 'text-red-600 dark:text-red-400 mb-4' : 'text-orange-600 dark:text-orange-400 mb-4'}>
|
||||
<p className="font-medium mb-2">{warning.title}</p>
|
||||
<ul className="text-sm space-y-1 ml-4">
|
||||
{warning.items.map((item, idx) => (
|
||||
<li key={idx}>• {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
{warning.footer && (
|
||||
<p className="font-bold mt-3 text-red-700 dark:text-red-300">
|
||||
{warning.footer}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isHardDelete && canDelete && requireConfirmText && (
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
|
||||
Para confirmar, escriba <span className="font-mono bg-[var(--background-secondary)] px-1 rounded text-[var(--text-primary)]">{customConfirmText}</span>:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--background-primary)] text-[var(--text-primary)] rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 placeholder:text-[var(--text-tertiary)]"
|
||||
placeholder={`Escriba ${customConfirmText}`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Volver
|
||||
</Button>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant={isHardDelete ? 'danger' : 'warning'}
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={isConfirmDisabled || isLoading || dependencyCheck?.isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{isHardDelete ? 'Eliminar Permanentemente' : 'Desactivar'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial mode selection
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} size="lg">
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="bg-[var(--background-secondary)] p-4 rounded-lg">
|
||||
<p className="font-medium text-[var(--text-primary)]">{entityDisplay.primaryText}</p>
|
||||
{entityDisplay.secondaryText && (
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{entityDisplay.secondaryText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-[var(--text-primary)] mb-4">
|
||||
Elija el tipo de eliminación que desea realizar:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Soft Delete Option */}
|
||||
<div
|
||||
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
|
||||
selectedMode === 'soft'
|
||||
? 'border-orange-500 bg-orange-50 dark:bg-orange-900/10'
|
||||
: 'border-[var(--border-color)] hover:border-orange-300'
|
||||
}`}
|
||||
onClick={() => setSelectedMode('soft')}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<div className={`w-4 h-4 rounded-full border-2 ${
|
||||
selectedMode === 'soft'
|
||||
? 'border-orange-500 bg-orange-500'
|
||||
: 'border-[var(--border-color)]'
|
||||
}`}>
|
||||
{selectedMode === 'soft' && (
|
||||
<div className="w-2 h-2 bg-white rounded-full m-0.5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-[var(--text-primary)] mb-1">
|
||||
{softDeleteOption.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{softDeleteOption.description}
|
||||
</p>
|
||||
<div className="mt-2 text-xs text-orange-600 dark:text-orange-400">
|
||||
{softDeleteOption.benefits}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hard Delete Option */}
|
||||
{hardDeleteOption && (
|
||||
<div
|
||||
className={`border-2 rounded-lg p-4 transition-colors ${
|
||||
!isHardDeleteEnabled
|
||||
? 'opacity-50 cursor-not-allowed border-[var(--border-color)] bg-[var(--background-tertiary)]'
|
||||
: `cursor-pointer ${
|
||||
selectedMode === 'hard'
|
||||
? 'border-red-500 bg-red-50 dark:bg-red-900/10'
|
||||
: 'border-[var(--border-color)] hover:border-red-300'
|
||||
}`
|
||||
}`}
|
||||
onClick={() => isHardDeleteEnabled && setSelectedMode('hard')}
|
||||
title={!isHardDeleteEnabled ? hardDeleteOption.disabledMessage : undefined}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<div className={`w-4 h-4 rounded-full border-2 ${
|
||||
selectedMode === 'hard' && isHardDeleteEnabled
|
||||
? 'border-red-500 bg-red-500'
|
||||
: 'border-[var(--border-color)]'
|
||||
}`}>
|
||||
{selectedMode === 'hard' && isHardDeleteEnabled && (
|
||||
<div className="w-2 h-2 bg-white rounded-full m-0.5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-[var(--text-primary)] mb-1 flex items-center gap-2">
|
||||
{hardDeleteOption.title}
|
||||
<AlertTriangle className="w-4 h-4 text-red-500" />
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{hardDeleteOption.description}
|
||||
</p>
|
||||
<div className={`mt-2 text-xs ${
|
||||
isHardDeleteEnabled
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-[var(--text-tertiary)]'
|
||||
}`}>
|
||||
{isHardDeleteEnabled ? hardDeleteOption.benefits : `ℹ️ ${hardDeleteOption.disabledMessage}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedMode === 'hard' && isHardDeleteEnabled ? 'danger' : 'warning'}
|
||||
onClick={() => handleDeleteModeSelect(selectedMode)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Continuar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default BaseDeleteModal;
|
||||
9
frontend/src/components/ui/BaseDeleteModal/index.ts
Normal file
9
frontend/src/components/ui/BaseDeleteModal/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { BaseDeleteModal } from './BaseDeleteModal';
|
||||
export type {
|
||||
DeleteMode,
|
||||
EntityDisplayInfo,
|
||||
DeleteModeOption,
|
||||
DeleteWarning,
|
||||
DeletionSummaryData,
|
||||
BaseDeleteModalProps,
|
||||
} from './BaseDeleteModal';
|
||||
@@ -144,11 +144,7 @@ const renderEditableField = (
|
||||
onChange?: (value: string | number) => void,
|
||||
validationError?: string
|
||||
): React.ReactNode => {
|
||||
if (!isEditMode || !field.editable) {
|
||||
return formatFieldValue(field.value, field.type);
|
||||
}
|
||||
|
||||
// Handle custom components
|
||||
// Handle custom components FIRST - they work in both view and edit modes
|
||||
if (field.type === 'component' && field.component) {
|
||||
const Component = field.component;
|
||||
return (
|
||||
@@ -160,6 +156,11 @@ const renderEditableField = (
|
||||
);
|
||||
}
|
||||
|
||||
// Then check if we should render as view or edit
|
||||
if (!isEditMode || !field.editable) {
|
||||
return formatFieldValue(field.value, field.type);
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = field.type === 'number' || field.type === 'currency' ? Number(e.target.value) : e.target.value;
|
||||
onChange?.(value);
|
||||
@@ -355,6 +356,16 @@ export const EditViewModal: React.FC<EditViewModalProps> = ({
|
||||
const StatusIcon = statusIndicator?.icon;
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
const [isWaitingForRefetch, setIsWaitingForRefetch] = React.useState(false);
|
||||
const [collapsedSections, setCollapsedSections] = React.useState<Record<number, boolean>>({});
|
||||
|
||||
// Initialize collapsed states when sections change
|
||||
React.useEffect(() => {
|
||||
const initialCollapsed: Record<number, boolean> = {};
|
||||
sections.forEach((section, index) => {
|
||||
initialCollapsed[index] = section.collapsed || false;
|
||||
});
|
||||
setCollapsedSections(initialCollapsed);
|
||||
}, [sections]);
|
||||
|
||||
const handleEdit = () => {
|
||||
if (onModeChange) {
|
||||
@@ -616,7 +627,7 @@ export const EditViewModal: React.FC<EditViewModalProps> = ({
|
||||
|
||||
<div className="space-y-6">
|
||||
{sections.map((section, sectionIndex) => {
|
||||
const [isCollapsed, setIsCollapsed] = React.useState(section.collapsed || false);
|
||||
const isCollapsed = collapsedSections[sectionIndex] || false;
|
||||
const sectionColumns = section.columns || (mobileOptimized ? 1 : 2);
|
||||
|
||||
// Determine grid classes based on mobile optimization and section columns
|
||||
@@ -642,7 +653,12 @@ export const EditViewModal: React.FC<EditViewModalProps> = ({
|
||||
className={`flex items-start gap-3 pb-3 border-b border-[var(--border-primary)] ${
|
||||
section.collapsible ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
onClick={section.collapsible ? () => setIsCollapsed(!isCollapsed) : undefined}
|
||||
onClick={section.collapsible ? () => {
|
||||
setCollapsedSections(prev => ({
|
||||
...prev,
|
||||
[sectionIndex]: !isCollapsed
|
||||
}));
|
||||
} : undefined}
|
||||
>
|
||||
{section.icon && (
|
||||
<section.icon className="w-5 h-5 text-[var(--text-secondary)] flex-shrink-0 mt-0.5" />
|
||||
|
||||
@@ -27,6 +27,7 @@ export { LoadingSpinner } from './LoadingSpinner';
|
||||
export { EmptyState } from './EmptyState';
|
||||
export { ResponsiveText } from './ResponsiveText';
|
||||
export { SearchAndFilter } from './SearchAndFilter';
|
||||
export { BaseDeleteModal } from './BaseDeleteModal';
|
||||
|
||||
// Export types
|
||||
export type { ButtonProps } from './Button';
|
||||
@@ -54,4 +55,5 @@ export type { DialogModalProps, DialogModalAction } from './DialogModal';
|
||||
export type { LoadingSpinnerProps } from './LoadingSpinner';
|
||||
export type { EmptyStateProps } from './EmptyState';
|
||||
export type { ResponsiveTextProps } from './ResponsiveText';
|
||||
export type { SearchAndFilterProps, FilterConfig, FilterOption } from './SearchAndFilter';
|
||||
export type { SearchAndFilterProps, FilterConfig, FilterOption } from './SearchAndFilter';
|
||||
export type { BaseDeleteModalProps, DeleteMode, EntityDisplayInfo, DeleteModeOption, DeleteWarning, DeletionSummaryData } from './BaseDeleteModal';
|
||||
Reference in New Issue
Block a user