import * as React from 'react'; import { alpha } from '@mui/material/styles'; import { Box, Typography, Button, IconButton, Tooltip, Card, CardContent, CardActions, Grid, Menu, MenuItem, useMediaQuery, useTheme, Divider, Chip, Stack, } from '@mui/material'; import { DataGrid, GridColDef, GridActionsCellItem, GridRenderCellParams, GridPaginationModel, } from '@mui/x-data-grid'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import VisibilityIcon from '@mui/icons-material/Visibility'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import { useNavigate } from 'react-router-dom'; import { ResourceConfig } from '../types/config'; import { EnhancedTableComponents } from '../types/overrides'; import { getFieldOptions, toGridValueOptions, resolveTemplate } from '../utils/options'; interface EnhancedTableProps { config: ResourceConfig; data: any[]; total?: number; paginationModel?: GridPaginationModel; onPaginationModelChange?: (model: GridPaginationModel) => void; loading?: boolean; onEdit: (item: any) => void; onDelete: (id: string) => void; onCreate: () => void; onNavigateToResource?: (resourceName: string, id: string) => void; components?: EnhancedTableComponents; } export default function EnhancedTable({ config, data, total, paginationModel: externalPaginationModel, onPaginationModelChange: externalOnPaginationModelChange, loading = false, onEdit, onDelete, onCreate, onNavigateToResource, components: tableComponents, }: EnhancedTableProps) { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const navigate = useNavigate(); const isServer = config.filterOptions?.mode !== "client"; const [internalPaginationModel, setInternalPaginationModel] = React.useState({ page: 0, pageSize: 10, }); const paginationModel = isServer ? externalPaginationModel : internalPaginationModel; const onPaginationModelChange = isServer ? externalOnPaginationModelChange : setInternalPaginationModel; const columns: GridColDef[] = React.useMemo(() => { const cols: GridColDef[] = Object.entries(config.fields).map(([key, field]) => { let muiType: 'string' | 'number' | 'boolean' | 'date' | 'dateTime' | 'singleSelect' = 'string'; if (field.type === 'number') muiType = 'number'; if (field.type === 'boolean') muiType = 'boolean'; if (field.type === 'date') muiType = 'date'; if (field.type === 'datetime') muiType = 'dateTime'; if (field.type === 'enum') muiType = 'singleSelect'; const col: GridColDef = { field: key, headerName: field.label, type: muiType, flex: 1, minWidth: 150, renderCell: (params: GridRenderCellParams) => }; if (muiType === 'date' || muiType === 'dateTime') { col.valueGetter = (value: any) => { if (!value) return null; const date = new Date(value); return isNaN(date.getTime()) ? null : date; }; } if (muiType === 'singleSelect') { (col as GridColDef & { valueOptions: any[] }).valueOptions = toGridValueOptions(getFieldOptions(field)); } return col; }); cols.push({ field: 'actions', type: 'actions', headerName: 'Actions', width: 120, getActions: (params) => [ } label="View" onClick={() => navigate(`/admin/${config.name}/${params.id}`)} />, } label="Edit" onClick={() => navigate(`/admin/${config.name}/edit/${params.id}`)} />, } label="Delete" onClick={() => onDelete(params.id as string)} />, ], }); return cols; }, [config, onDelete, navigate, onNavigateToResource]); const mobilePageSize = 10; const [mobilePage, setMobilePage] = React.useState(0); const mobileTotalPages = Math.ceil(data.length / mobilePageSize) || 1; const mobileData = data.slice(mobilePage * mobilePageSize, (mobilePage + 1) * mobilePageSize); React.useEffect(() => { if (mobilePage >= mobileTotalPages) setMobilePage(0); }, [data.length, mobilePage, mobileTotalPages]); if (isMobile) { return ( {config.pluralLabel} {mobileData.map((row) => ( ))} Page {mobilePage + 1} of {mobileTotalPages} ); } return ( {config.pluralLabel} { if (total !== undefined) return total; const page = paginationModel?.page || 0; const pageSize = paginationModel?.pageSize || 10; if (data.length < pageSize) { return page * pageSize + data.length; } return (page + 2) * pageSize; })(), } : {})} loading={loading} paginationModel={paginationModel || { page: 0, pageSize: 10 }} onPaginationModelChange={onPaginationModelChange} getRowId={(row) => { const pk = config.primaryKey; if (row[pk] !== undefined && row[pk] !== null) return row[pk]; const fallbackKeys = ['id', '_id', 'uuid', 'pk']; for (const key of fallbackKeys) { if (row[key] !== undefined && row[key] !== null) return row[key]; } return `temp-id-${data.indexOf(row)}`; }} disableRowSelectionOnClick pageSizeOptions={[10, 25, 50]} sx={{ border: 'none', '& .MuiDataGrid-cell:focus': { outline: 'none' }, '& .MuiDataGrid-columnHeader:focus': { outline: 'none' }, }} /> ); } function MobileCardRow({ row, config, onDelete, onNavigate, navigate, components }: any) { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); const id = row[config.primaryKey]; const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; return ( #{id} { handleClose(); navigate(`/admin/${config.name}/${id}`); }}>View { handleClose(); navigate(`/admin/${config.name}/edit/${id}`); }}>Edit { handleClose(); onDelete(id); }} sx={{ color: 'error.main' }}>Delete {Object.entries(config.fields).slice(0, 5).map(([key, field]: [string, any]) => ( {field.label} ))} ); } function getFormattedDisplayValue(item: any, displayFormat: string) { if (!item) return ""; return resolveTemplate(displayFormat, item); } function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile, components }: any) { const value = params.value; const isPk = fieldKey === config.primaryKey; if (field.formatter) return field.formatter(value); const customRenderer = components?.cellRenderers?.[field.type as string]; if (customRenderer) { return React.createElement(customRenderer, { value, row: params.row, field, fieldKey, config, onNavigate, isMobile }); } // 1. Single Relation if (field.relation && value && !Array.isArray(value)) { const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value; const displayValue = getFormattedDisplayValue(value, field.displayFormat); return ( { e.stopPropagation(); if (relationId) onNavigate?.(field.relation!, String(relationId)); }} sx={{ cursor: 'pointer' }} /> ); } // 2. Multi-Select (Array of relations or simple strings) if (field.type === 'array' && Array.isArray(value)) { const enumValue = field.enumOption?.value; const tooltipTitle = value.map((item) => getFormattedDisplayValue(item, field.displayFormat)).join(', '); return ( {value.map((item, idx) => ( { e.stopPropagation(); if (field.relation) { const id = typeof item === 'object' ? (item.id || item._id) : item; if (id) onNavigate?.(field.relation!, String(id)); } }} /> ))} ); } // 3. Simple Objects if (field.type === 'object' && value) { return getFormattedDisplayValue(value, field.displayFormat) || (isMobile ? 'Object' : JSON.stringify(value)); } if (field.type === 'number' && typeof value === 'number') { const isNegative = value < 0; const color = isNegative ? 'error' : 'success'; return ( alpha(theme.palette[color].main, 0.15), color: (theme) => theme.palette[color].dark, '& .MuiChip-label': { px: 1.5 } }} /> ); } if (field.type === 'boolean') { return value ? ( ) : ( ); } if (field.type === 'datetime') return value ? new Date(value).toLocaleString() : ''; if (field.type === 'date') return value ? new Date(value).toLocaleDateString() : ''; if (field.type === 'enum') { const opt = getFieldOptions(field).find(o => o.key === value); return opt?.value ?? value; } if (isPk && !isMobile) { return ( { e.stopPropagation(); navigate(`/admin/${config.name}/${params.row[config.primaryKey]}`); }} sx={{ cursor: 'pointer', fontWeight: 'bold' }} /> ); } return value; }