responsive table now has optional mobile render prop; users now has functional mobile view

This commit is contained in:
Carter 2024-11-26 11:51:11 -06:00
parent 6234f13eb0
commit 3f96724422
5 changed files with 238 additions and 8 deletions

87
package-lock.json generated
View file

@ -14,6 +14,7 @@
"@mui/icons-material": "^6.1.6",
"@mui/material": "^6.1.6",
"@mui/styles": "^6.1.6",
"@mui/x-date-pickers": "^7.22.3",
"@sentry/react": "^8.38.0",
"@types/classnames": "^2.3.0",
"axios": "^1.7.7",
@ -1510,6 +1511,92 @@
}
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.22.3",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.3.tgz",
"integrity": "sha512-shNp92IrST5BiVy2f4jbrmRaD32QhyUthjh1Oexvpcn0v6INyuWgxfodoTi5ZCnE5Ue5UVFSs4R9Xre0UbJ5DQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.21.0",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0",
"@mui/system": "^5.15.14 || ^6.0.0",
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
"moment": "^2.29.4",
"moment-hijri": "^2.1.2 || ^3.0.0",
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"date-fns": {
"optional": true
},
"date-fns-jalali": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
},
"moment-hijri": {
"optional": true
},
"moment-jalaali": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.21.0.tgz",
"integrity": "sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View file

@ -23,6 +23,7 @@
"@mui/icons-material": "^6.1.6",
"@mui/material": "^6.1.6",
"@mui/styles": "^6.1.6",
"@mui/x-date-pickers": "^7.22.3",
"@sentry/react": "^8.38.0",
"@types/classnames": "^2.3.0",
"axios": "^1.7.7",

View file

@ -1,9 +1,10 @@
import { Box, CircularProgress, Collapse, darken, Grid2, IconButton, InputAdornment, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material";
import { Box, Button, Card, CircularProgress, Collapse, darken, Divider, Grid2, IconButton, InputAdornment, List, ListItem, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import { ChevronRight, Close, Search } from "@mui/icons-material"
import { useEffect, useState } from "react";
import React from "react";
import { useMobile } from "hooks";
import { render } from "react-dom";
const useStyles = makeStyles((theme: Theme) => {
// const isMobile = useMobile()
@ -29,13 +30,22 @@ const useStyles = makeStyles((theme: Theme) => {
border: "none",
textAlign: "right",
},
mobileTitleBox: {
margin: theme.spacing(1),
marginBottom: theme.spacing(1.5),
},
card: {
// padding: theme.spacing(1),
margin: theme.spacing(1),
}
})},
);
export interface Column<T> {
title: string | JSX.Element;
cellStyle?: SxProps<Theme> | undefined
render?: (row: T) => JSX.Element;
cellStyle?: SxProps<Theme>;
render: (row: T) => JSX.Element;
}
interface Props<T> {
@ -53,6 +63,7 @@ interface Props<T> {
multiGutter?: boolean;
rowsPerPageOptions?: number[];
noDataMessage?: string;
renderMobile?: (row: T) => JSX.Element;
}
export default function ResponsiveTable<T extends Object>(props: Props<T>) {
@ -71,6 +82,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
columns,
rowsPerPageOptions,
noDataMessage,
renderMobile,
} = props
const classes = useStyles();
@ -125,6 +137,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
const searchBar = () => {
return (
<TextField
fullWidth
variant="standard"
placeholder="Search..."
className={classes.title}
@ -149,10 +162,116 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
)
}
const renderTitle = () => {
if (typeof(title) === "string" ) return (
<Typography variant="h5" className={classes.title}>
{title}
</Typography>
)
if (title) return (
<Box className={classes.title}>
title
</Box>
)
return null
}
function capitalize(val: any) {
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
}
const renderMobileRow = (row: T) => {
if (renderMobile) return (
renderMobile(row)
)
if (columns) {
return (
<Grid2 container direction={"row"} spacing={2} padding={2}>
{columns.map((column, index) => {
return (
<React.Fragment key={"mobile-row-column-"+index}>
<Grid2 size={{ xs: 3 }} alignContent={"center"}>
<Typography fontWeight={"bold"} >
{column.title+": "}
</Typography>
</Grid2>
<Grid2 size={{ xs: 9 }} alignContent={"center"}>
{column.render(row)}
</Grid2>
</React.Fragment>
)
})}
</Grid2>
)
}
return (
Object.keys(row).map((key, index) => {
let value = row[key as keyof typeof row]
return (
<Grid2 container direction={"row"} key={"mobile-list-"+index} spacing={1} padding={1}>
<Grid2 size={{ xs: 4 }} alignContent={"center"} >
<Typography fontWeight={"bold"} >
{capitalize(key)+": "}
</Typography>
</Grid2>
<Grid2 size={{ xs: 8 }}>
<Typography>
{value+""}
</Typography>
</Grid2>
</Grid2>
)
})
)
}
if (isMobile) return (
<Typography>
Hello!
</Typography>
<Box>
<Box className={classes.mobileTitleBox}>
{renderTitle()}
{setSearchText && searchBar()}
</Box>
{rows.map((row, index) => {
return (
<Card className={classes.card} key={"mobile-card-"+index}>
{renderMobileRow(row)}
{renderGutter &&
<>
<Divider/>
<Collapse className={classes.gutter} in={openGutters.includes(index)} >
{renderGutter(row)}
</Collapse>
{openGutters.includes(index) && <Divider/>}
<IconButton
onClick={() => handleCollapse(index)}
sx={{
transform: openGutters.includes(index) ?
'rotate(-90deg)' : 'rotate(90deg)',
transition: 'transform 0.3s ease',
width: "100%",
}}
>
<ChevronRight/>
</IconButton>
</>
}
</Card>
)
})}
{rows.length < 1 &&
(!isLoading ?
<Typography sx={{ textAlign: "center" }} color="textSecondary">
{ noDataMessage ? noDataMessage : "No data."}
</Typography>
:
<Box sx={{ width: "100%", textAlign: "center" }} >
<CircularProgress/>
</Box>
)
}
</Box>
)
return (
@ -222,6 +341,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
transform: openGutters.includes(index) ?
'rotate(90deg)' : 'rotate(0deg)',
transition: 'transform 0.3s ease',
marginRight: -1
}}
onClick={() => handleCollapse(index)}>
<ChevronRight/>

View file

@ -1,5 +1,6 @@
import {
Avatar,
Box,
Button,
Chip,
darken,
@ -326,9 +327,28 @@ export default function Users() {
);
};
const renderMobile = (user: User) => {
const avatarURL = user.settings.avatar;
const avatar = avatarURL ? (
<Avatar alt={user.name()} src={avatarURL} />
) : (
<Avatar alt={user.name()}>
<Face />
</Avatar>
);
return (
<Box sx={{padding: 1}}>
<ListItem>
<ListItemAvatar>{avatar}</ListItemAvatar>
<ListItemText primary={user.name()} secondary={user.settings.email} />
</ListItem>
</Box>
)
}
return (
<PageContainer>
<ResponsiveTable<User>
<ResponsiveTable<User>
title="Users"
rows={rows}
columns={columns()}
@ -340,6 +360,7 @@ export default function Users() {
setSearchText={setSearchText}
isLoading={isLoading}
renderGutter={details}
renderMobile={renderMobile}
// multiGutter
/>
<ResponsiveDialog

View file

@ -509,7 +509,8 @@ export default function ShareObject(props: Props) {
// inputRef={props => <TextField {...props} helperText="" />}
label="Expiration Date"
value={newLinkExpiration}
onChange={date => setNewLinkExpiration(date as Moment)}
// onChange={(date: moment.Moment) => setNewLinkExpiration(date as Moment)}
onChange={(value) => setNewLinkExpiration(value as Moment)}
disablePast
/>
<FormControlLabel