mobile view
This commit is contained in:
@@ -14,10 +14,13 @@ import {
|
|||||||
CssBaseline,
|
CssBaseline,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import TableViewIcon from '@mui/icons-material/TableView';
|
import TableViewIcon from '@mui/icons-material/TableView';
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
import { ResourceConfig } from '../types/config';
|
import { ResourceConfig } from '../types/config';
|
||||||
@@ -41,26 +44,108 @@ export default function AdminLayout({
|
|||||||
username,
|
username,
|
||||||
resources,
|
resources,
|
||||||
}: AdminLayoutProps) {
|
}: AdminLayoutProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
||||||
|
const [mobileOpen, setMobileOpen] = React.useState(false);
|
||||||
|
|
||||||
const activeResourceName = location.pathname.split('/')[1] || null;
|
const activeResourceName = location.pathname.split('/')[1] || null;
|
||||||
|
|
||||||
// AUTO-TOGGLE LOGIC
|
// AUTO-TOGGLE LOGIC
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (isMobile) {
|
||||||
|
setIsCollapsed(false); // Mobile drawer is never "mini"
|
||||||
|
setMobileOpen(false); // Close on navigation
|
||||||
|
} else {
|
||||||
if (location.pathname === '/' || location.pathname === '') {
|
if (location.pathname === '/' || location.pathname === '') {
|
||||||
setIsCollapsed(false);
|
setIsCollapsed(false);
|
||||||
} else {
|
} else {
|
||||||
setIsCollapsed(true);
|
setIsCollapsed(true);
|
||||||
}
|
}
|
||||||
}, [location.pathname]);
|
}
|
||||||
|
}, [location.pathname, isMobile]);
|
||||||
|
|
||||||
const currentWidth = isCollapsed ? collapsedWidth : drawerWidth;
|
const currentWidth = isMobile ? drawerWidth : (isCollapsed ? collapsedWidth : drawerWidth);
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleDrawerToggle = () => {
|
||||||
|
setMobileOpen(!mobileOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSidebarToggle = () => {
|
||||||
setIsCollapsed(!isCollapsed);
|
setIsCollapsed(!isCollapsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const drawerContent = (
|
||||||
|
<Box sx={{ overflow: 'hidden', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
{!isMobile && (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: isCollapsed ? 'center' : 'flex-end', p: 1 }}>
|
||||||
|
<IconButton onClick={handleSidebarToggle}>
|
||||||
|
{isCollapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isMobile && <Toolbar />}
|
||||||
|
|
||||||
|
<List>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<Tooltip title={(isCollapsed && !isMobile) ? "Dashboard" : ""} placement="right">
|
||||||
|
<ListItemButton
|
||||||
|
selected={location.pathname === '/'}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
sx={{
|
||||||
|
minHeight: 48,
|
||||||
|
justifyContent: (isCollapsed && !isMobile) ? 'center' : 'initial',
|
||||||
|
px: 2.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{
|
||||||
|
minWidth: 0,
|
||||||
|
mr: (isCollapsed && !isMobile) ? 0 : 3,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<DashboardIcon color={location.pathname === '/' ? 'primary' : 'inherit'} />
|
||||||
|
</ListItemIcon>
|
||||||
|
{(!isCollapsed || isMobile) && <ListItemText primary="Dashboard" />}
|
||||||
|
</ListItemButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List sx={{ flexGrow: 1 }}>
|
||||||
|
{resources.map((res) => (
|
||||||
|
<ListItem key={res.name} disablePadding>
|
||||||
|
<Tooltip title={(isCollapsed && !isMobile) ? res.pluralLabel : ""} placement="right">
|
||||||
|
<ListItemButton
|
||||||
|
selected={activeResourceName === res.name}
|
||||||
|
onClick={() => onSelectResource(res.name)}
|
||||||
|
sx={{
|
||||||
|
minHeight: 48,
|
||||||
|
justifyContent: (isCollapsed && !isMobile) ? 'center' : 'initial',
|
||||||
|
px: 2.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{
|
||||||
|
minWidth: 0,
|
||||||
|
mr: (isCollapsed && !isMobile) ? 0 : 3,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<TableViewIcon color={activeResourceName === res.name ? 'primary' : 'inherit'} />
|
||||||
|
</ListItemIcon>
|
||||||
|
{(!isCollapsed || isMobile) && <ListItemText primary={res.pluralLabel} />}
|
||||||
|
</ListItemButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
@@ -69,7 +154,7 @@ export default function AdminLayout({
|
|||||||
sx={{
|
sx={{
|
||||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||||
backdropFilter: 'blur(8px)',
|
backdropFilter: 'blur(8px)',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.8)', // Adjust based on theme in real app
|
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
color: 'text.primary',
|
color: 'text.primary',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
borderBottom: '1px solid',
|
borderBottom: '1px solid',
|
||||||
@@ -77,10 +162,21 @@ export default function AdminLayout({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
{isMobile && (
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={handleDrawerToggle}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1, fontWeight: 'bold' }}>
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1, fontWeight: 'bold' }}>
|
||||||
Admin Panel
|
Admin Panel
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ mr: 2, fontWeight: 500 }}>
|
<Typography variant="body1" sx={{ mr: 2, fontWeight: 500, display: { xs: 'none', sm: 'block' } }}>
|
||||||
{username}
|
{username}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title="Logout">
|
<Tooltip title="Logout">
|
||||||
@@ -90,14 +186,33 @@ export default function AdminLayout({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
component="nav"
|
||||||
|
sx={{ width: { md: currentWidth }, flexShrink: { md: 0 } }}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
open={mobileOpen}
|
||||||
|
onClose={handleDrawerToggle}
|
||||||
|
ModalProps={{ keepMounted: true }}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' },
|
||||||
|
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawerContent}
|
||||||
|
</Drawer>
|
||||||
|
) : (
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
sx={{
|
sx={{
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
width: currentWidth,
|
width: currentWidth,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
// TRANSITION
|
|
||||||
transition: (theme) => theme.transitions.create('width', {
|
transition: (theme) => theme.transitions.create('width', {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
@@ -112,73 +227,19 @@ export default function AdminLayout({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
open
|
||||||
>
|
>
|
||||||
<Toolbar />
|
{drawerContent}
|
||||||
<Box sx={{ overflow: 'hidden', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: isCollapsed ? 'center' : 'flex-end', p: 1 }}>
|
|
||||||
<IconButton onClick={handleToggle}>
|
|
||||||
{isCollapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Divider />
|
|
||||||
<List>
|
|
||||||
<ListItem disablePadding>
|
|
||||||
<Tooltip title={isCollapsed ? "Dashboard" : ""} placement="right">
|
|
||||||
<ListItemButton
|
|
||||||
selected={location.pathname === '/'}
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
sx={{
|
|
||||||
minHeight: 48,
|
|
||||||
justifyContent: isCollapsed ? 'center' : 'initial',
|
|
||||||
px: 2.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon sx={{
|
|
||||||
minWidth: 0,
|
|
||||||
mr: isCollapsed ? 0 : 3,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}>
|
|
||||||
<DashboardIcon color={location.pathname === '/' ? 'primary' : 'inherit'} />
|
|
||||||
</ListItemIcon>
|
|
||||||
{!isCollapsed && <ListItemText primary="Dashboard" />}
|
|
||||||
</ListItemButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
<Divider />
|
|
||||||
<List sx={{ flexGrow: 1 }}>
|
|
||||||
{resources.map((res) => (
|
|
||||||
<ListItem key={res.name} disablePadding>
|
|
||||||
<Tooltip title={isCollapsed ? res.pluralLabel : ""} placement="right">
|
|
||||||
<ListItemButton
|
|
||||||
selected={activeResourceName === res.name}
|
|
||||||
onClick={() => onSelectResource(res.name)}
|
|
||||||
sx={{
|
|
||||||
minHeight: 48,
|
|
||||||
justifyContent: isCollapsed ? 'center' : 'initial',
|
|
||||||
px: 2.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemIcon sx={{
|
|
||||||
minWidth: 0,
|
|
||||||
mr: isCollapsed ? 0 : 3,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}>
|
|
||||||
<TableViewIcon color={activeResourceName === res.name ? 'primary' : 'inherit'} />
|
|
||||||
</ListItemIcon>
|
|
||||||
{!isCollapsed && <ListItemText primary={res.pluralLabel} />}
|
|
||||||
</ListItemButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
p: 3,
|
p: { xs: 2, md: 3 },
|
||||||
|
width: { xs: '100%', md: `calc(100% - ${currentWidth}px)` },
|
||||||
transition: (theme) => theme.transitions.create(['margin', 'width'], {
|
transition: (theme) => theme.transitions.create(['margin', 'width'], {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardActions,
|
||||||
|
Grid,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
Divider,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
DataGrid,
|
DataGrid,
|
||||||
@@ -16,6 +25,7 @@ import {
|
|||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ResourceConfig } from '../types/config';
|
import { ResourceConfig } from '../types/config';
|
||||||
|
|
||||||
@@ -36,6 +46,8 @@ export default function EnhancedTable({
|
|||||||
onCreate,
|
onCreate,
|
||||||
onNavigateToResource,
|
onNavigateToResource,
|
||||||
}: EnhancedTableProps) {
|
}: EnhancedTableProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const columns: GridColDef[] = React.useMemo(() => {
|
const columns: GridColDef[] = React.useMemo(() => {
|
||||||
@@ -45,87 +57,7 @@ export default function EnhancedTable({
|
|||||||
headerName: field.label,
|
headerName: field.label,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => <FieldRenderer params={params} field={field} fieldKey={key} config={config} onNavigate={onNavigateToResource} navigate={navigate} />
|
||||||
const value = params.value;
|
|
||||||
|
|
||||||
// 0. Link to View if it's the Primary Key (if it's displayed)
|
|
||||||
const isPk = key === config.primaryKey;
|
|
||||||
|
|
||||||
// 1. Custom Formatter
|
|
||||||
if (field.formatter) {
|
|
||||||
return field.formatter(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Relational Link
|
|
||||||
if (field.relation && value) {
|
|
||||||
const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value;
|
|
||||||
const displayValue =
|
|
||||||
typeof value === "object"
|
|
||||||
? (
|
|
||||||
(field?.displayField && (value as Record<string, any>)[field.displayField]) ||
|
|
||||||
(value as any).id ||
|
|
||||||
(value as any)._id ||
|
|
||||||
(value as any).pk
|
|
||||||
)
|
|
||||||
: value;
|
|
||||||
if (relationId) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onNavigateToResource?.(field.relation!, String(relationId));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{displayValue}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Nested Object / Array Display
|
|
||||||
if (field.type === 'array' && Array.isArray(value)) {
|
|
||||||
if (field.displayField) {
|
|
||||||
return value
|
|
||||||
.map((item) => (typeof item === 'object' ? item[field.displayField!] : item))
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ');
|
|
||||||
}
|
|
||||||
return `${value.length} items`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.type === 'object' && value) {
|
|
||||||
if (field.displayField && value[field.displayField]) {
|
|
||||||
return value[field.displayField];
|
|
||||||
}
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Default renderings
|
|
||||||
if (field.type === 'boolean') return value ? 'Yes' : 'No';
|
|
||||||
if (field.type === 'datetime' || field.type === 'date') {
|
|
||||||
return (value) ? new Date(value).toLocaleString() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPk) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const rowId = params.row[config.primaryKey];
|
|
||||||
navigate(`/${config.name}/${rowId}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
return col;
|
return col;
|
||||||
});
|
});
|
||||||
@@ -157,10 +89,37 @@ export default function EnhancedTable({
|
|||||||
return cols;
|
return cols;
|
||||||
}, [config, onDelete, navigate, onNavigateToResource]);
|
}, [config, onDelete, navigate, onNavigateToResource]);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2, alignItems: 'center' }}>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>{config.pluralLabel}</Typography>
|
||||||
|
<Button variant="contained" color="primary" onClick={onCreate} size="small">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{data.map((row) => (
|
||||||
|
<Grid item xs={12} key={row[config.primaryKey] || Math.random()}>
|
||||||
|
<MobileCardRow
|
||||||
|
row={row}
|
||||||
|
config={config}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onNavigate={onNavigateToResource}
|
||||||
|
navigate={navigate}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3, alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3, alignItems: 'center' }}>
|
||||||
<Typography variant="h5">{config.pluralLabel}</Typography>
|
<Typography variant="h5" sx={{ fontWeight: 'bold' }}>{config.pluralLabel}</Typography>
|
||||||
<Button variant="contained" color="primary" onClick={onCreate}>
|
<Button variant="contained" color="primary" onClick={onCreate}>
|
||||||
Add {config.label}
|
Add {config.label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -185,7 +144,107 @@ export default function EnhancedTable({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
pageSizeOptions={[10, 25, 50]}
|
pageSizeOptions={[10, 25, 50]}
|
||||||
|
sx={{
|
||||||
|
border: 'none',
|
||||||
|
'& .MuiDataGrid-cell:focus': { outline: 'none' },
|
||||||
|
'& .MuiDataGrid-columnHeader:focus': { outline: 'none' },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const id = row[config.primaryKey];
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card variant="outlined" sx={{ borderRadius: 2 }}>
|
||||||
|
<CardContent sx={{ pb: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||||
|
#{id}
|
||||||
|
</Typography>
|
||||||
|
<IconButton size="small" onClick={handleClick}>
|
||||||
|
<MoreVertIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||||
|
<MenuItem onClick={() => { handleClose(); navigate(`/${config.name}/${id}`); }}>View</MenuItem>
|
||||||
|
<MenuItem onClick={() => { handleClose(); navigate(`/${config.name}/edit/${id}`); }}>Edit</MenuItem>
|
||||||
|
<MenuItem onClick={() => { handleClose(); onDelete(id); }} sx={{ color: 'error.main' }}>Delete</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
{Object.entries(config.fields).slice(0, 5).map(([key, field]: [string, any]) => (
|
||||||
|
<Grid item xs={6} key={key}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||||
|
{field.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 500, wordBreak: 'break-all' }}>
|
||||||
|
<FieldRenderer params={{ value: row[key], row }} field={field} fieldKey={key} config={config} onNavigate={onNavigate} navigate={navigate} isMobile />
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions sx={{ justifyContent: 'flex-end', px: 2, pb: 2 }}>
|
||||||
|
<Button size="small" onClick={() => navigate(`/${config.name}/${id}`)}>View Details</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile }: any) {
|
||||||
|
const value = params.value;
|
||||||
|
const isPk = fieldKey === config.primaryKey;
|
||||||
|
|
||||||
|
if (field.formatter) return field.formatter(value);
|
||||||
|
|
||||||
|
if (field.relation && value) {
|
||||||
|
const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value;
|
||||||
|
const displayValue = typeof value === "object" ?
|
||||||
|
((field?.displayField && (value as Record<string, any>)[field.displayField]) || (value as any).id || (value as any)._id || (value as any).pk) : value;
|
||||||
|
|
||||||
|
if (relationId) {
|
||||||
|
return (
|
||||||
|
<Link component="button" variant="body2" sx={{ fontWeight: 'inherit', textAlign: 'left' }} onClick={(e) => { e.stopPropagation(); onNavigate?.(field.relation!, String(relationId)); }}>
|
||||||
|
{displayValue}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'array' && Array.isArray(value)) {
|
||||||
|
if (field.displayField) {
|
||||||
|
return value.map((item) => (typeof item === 'object' ? item[field.displayField!] : item)).filter(Boolean).join(', ');
|
||||||
|
}
|
||||||
|
return `${value.length} items`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'object' && value) {
|
||||||
|
if (field.displayField && value[field.displayField]) return value[field.displayField];
|
||||||
|
return isMobile ? 'Object' : JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'boolean') return value ? 'Yes' : 'No';
|
||||||
|
if (field.type === 'datetime' || field.type === 'date') return value ? new Date(value).toLocaleString() : '';
|
||||||
|
|
||||||
|
if (isPk && !isMobile) {
|
||||||
|
return (
|
||||||
|
<Link component="button" variant="body2" sx={{ fontWeight: 'inherit' }} onClick={(e) => { e.stopPropagation(); navigate(`/${config.name}/${params.row[config.primaryKey]}`); }}>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user