teams list now uses responsive container

This commit is contained in:
Carter 2024-11-26 15:28:49 -06:00
parent 7ee06c4e19
commit c94b8093ff
5 changed files with 147 additions and 261 deletions

View file

@ -50,7 +50,7 @@ export interface Column<T> {
interface Props<T> {
rows: T[],
columns?: Column<T>[],
title?: string;
title?: string | JSX.Element;
total: number,
pageSize: number,
page: number,
@ -63,6 +63,7 @@ interface Props<T> {
rowsPerPageOptions?: number[];
noDataMessage?: string;
renderMobile?: (row: T) => JSX.Element;
hideKeys?: boolean;
}
export default function ResponsiveTable<T extends Object>(props: Props<T>) {
@ -82,6 +83,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
rowsPerPageOptions,
noDataMessage,
renderMobile,
hideKeys,
} = props
const classes = useStyles();
@ -169,7 +171,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
)
if (title) return (
<Box className={classes.title}>
title
{title}
</Box>
)
return null
@ -185,16 +187,16 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
)
if (columns) {
return (
<Grid2 container direction={"row"} spacing={2} padding={2}>
<Grid2 container direction={"row"} spacing={1} >
{columns.map((column, index) => {
return (
<React.Fragment key={"mobile-row-column-"+index}>
<Grid2 size={{ xs: 3 }} alignContent={"center"}>
{ !hideKeys && <Grid2 size={{ xs: 3 }} alignContent={"center"}>
<Typography fontWeight={"bold"} >
{column.title+": "}
</Typography>
</Grid2>
<Grid2 size={{ xs: 9 }} alignContent={"center"}>
</Grid2>}
<Grid2 size={{ xs: hideKeys ? 12 : 9 }} alignContent={"center"} >
{column.render(row)}
</Grid2>
</React.Fragment>
@ -294,7 +296,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
</Typography>
:
title && <Box className={classes.title}>
title
{title}
</Box>
}
</Grid2>
@ -306,7 +308,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
</Grid2>
</TableCell>
</TableRow>
<TableRow>
{ !hideKeys && <TableRow>
{ renderGutter && <TableCell></TableCell>}
{ columns ?
columns.map((column, index) => {
@ -325,7 +327,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
)
})
}
</TableRow>
</TableRow>}
</TableHead>
<TableBody>
{ isLoading ?
@ -347,7 +349,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
marginRight: isMobile ? -1 : 0
}}
onClick={() => handleCollapse(index)}>
<ChevronRight/>
@ -357,7 +359,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
{columns ?
columns.map((column, j) => {
if (column.render) return (
<TableCell key={"row-"+index+"cell-"+j} sx={column.cellStyle}>
<TableCell key={"row-"+index+"cell-"+j} sx={{ padding: 0}} >
{column.render(row)}
</TableCell>
)
@ -369,9 +371,10 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
})
:
Object.values(row).map((value, j) => {
console
return (
<TableCell key={"row-"+index+"cell-"+j}>
{value.toString()}
{value ? value.toString() : ""}
</TableCell>
)
})

View file

@ -1,15 +1,10 @@
import { Box } from "@mui/material";
import { useMobile } from "hooks";
import PageContainer from "pages/PageContainer";
import TeamList from "teams/TeamList";
export default function Teams() {
const isMobile = useMobile();
return (
<PageContainer>
<Box paddingY={isMobile ? 1 : 1.5} paddingX={isMobile ? 1 : 2}>
<TeamList />
</Box>
</PageContainer>
);
}

View file

@ -239,7 +239,7 @@ export default function Users() {
</Avatar>
);
return (
<ListItem style={{ padding: 0 }}>
<ListItem sx={{ padding: 0 }}>
<ListItemAvatar>{avatar}</ListItemAvatar>
<ListItemText primary={user.name()} secondary={user.settings.email} />
</ListItem>
@ -252,7 +252,7 @@ export default function Users() {
render: user => {
const actions = user.settings.actions;
return (
<Grid2 container spacing={1} className={classes.chipContainer}>
<Grid2 container spacing={1} className={classes.chipContainer} padding={1}>
{actions.slice(0, 2).map(action => flagChip(action))}
{actions.length > 3 && flagChip("+" + (actions.length - 3) + " more")}
</Grid2>
@ -265,7 +265,7 @@ export default function Users() {
render: user => {
const features = user.settings.features;
return (
<Grid2 container spacing={1} className={classes.chipContainer}>
<Grid2 container spacing={1} className={classes.chipContainer} padding={1}>
{features.slice(0, 2).map(feature => flagChip(feature))}
{features.length > 3 && flagChip("+" + (features.length - 3) + " more")}
</Grid2>

View file

@ -1,38 +1,27 @@
import React, { useCallback, useEffect, useState } from "react";
import {
Card,
Theme,
List,
ListItem,
ListItemAvatar,
Avatar,
ListItemText,
Typography,
ListItemSecondaryAction,
Divider,
useTheme,
TextField,
InputAdornment,
CircularProgress,
ListItemButton,
Box,
IconButton,
MenuItem,
} from "@mui/material";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useTeamAPI, useUserAPI } from "providers";
import { Team, teamScope } from "models";
import { useNavigate } from "react-router";
// import TeamActions from "./TeamActions";
import SearchIcon from '@mui/icons-material/Search';
import NextIcon from "@mui/icons-material/NavigateNext";
import PrevIcon from "@mui/icons-material/NavigateBefore";
import LastIcon from "@mui/icons-material/SkipNext";
import FirstIcon from "@mui/icons-material/SkipPrevious";
import TeamSettings from "./TeamSettings";
import { AddBox } from "@mui/icons-material";
import { cloneDeep } from "lodash";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { makeStyles } from "@mui/styles";
import TeamActions from "./TeamActions";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import { useMobile } from "hooks";
import { AddBox } from "@mui/icons-material";
import TeamSettings from "./TeamSettings";
const useStyles = makeStyles((theme: Theme) => ({
cardContent: {
@ -54,96 +43,63 @@ const useStyles = makeStyles((theme: Theme) => ({
height: theme.spacing(6),
marginTop: "auto",
marginBottom: "auto"
},
titleBox: {
display: "flex",
alignItems: "center",
}
}))
const limits = [
{
value: 5,
label: "5"
},
{
value: 10,
label: "10"
},
{
value: 15,
label: "15"
},
{
value: 20,
label: "20"
}
];
export default function TeamList() {
const classes = useStyles();
const [teams, setTeams] = useState<pond.Team[]>([]);
const [teams, setTeams] = useState<Team[]>([]);
const teamAPI = useTeamAPI();
const userAPI = useUserAPI();
const history = useNavigate();
const theme = useTheme();
const isMobile = useMobile()
const [{ user, as }] = useGlobalState();
const [teamPerms, setTeamPerms] = useState<pond.Permission[][]>([]);
const [teamPrefs, setTeamPrefs] = useState<pond.TeamPreferences[]>([]);
const [teamPerms, setTeamPerms] = useState<Map<string | number, pond.Permission[]>>(new Map());
const [teamPrefs, setTeamPrefs] = useState<Map<string | number, pond.TeamPreferences> >(new Map());
const [total, setTotal] = useState<number>(0);
const [limit, setLimit] = useState<number>(5);
const [page, setPage] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [search, setSearch] = useState<string>("");
const [teamDialogOpen, setTeamDialogOpen] = useState<boolean>(false);
const [asUser, setAsUser] = useState<string | undefined>(undefined);
const [userId, setUserId] = useState<string | undefined>(undefined);
const [isUser, setIsUser] = useState<boolean>(true);
const [teamDialogOpen, setTeamDialogOpen] = useState<boolean>(false);
useEffect(() => {
if (as.includes("auth0")) setAsUser(as);
}, [as]);
useEffect(() => {
setUserId(user.id());
}, [user]);
useEffect(() => {
if (!asUser) {
setIsUser(true);
return;
}
setIsUser(userId === asUser);
}, [userId, asUser]);
// Convert string array to valid Permission[] array
// const convertToPermissions = (permissions: any[]): pond.Permission[] => {
// return permissions
// .map(p => p as any as pond.Permission) // Type assertion to treat strings as Permission
// .filter(p => Object.entries(pond.Permission).includes(p)); // Filter to ensure valid Permission values
// };
const loadTeams = useCallback(() => {
setLoading(true);
let newTeamPerms: pond.Permission[][] = [];
let newTeamPrefs: pond.TeamPreferences[] = [];
let newTeamPerms: Map<string | number, pond.Permission[]> = new Map();
let newTeamPrefs: Map<string | number, pond.TeamPreferences> = new Map();
let searchString = search;
teamAPI
.listTeams(limit, limit * page, undefined, "name", searchString, undefined, asUser)
.then(resp => {
setTotal(resp.data.total);
resp.data.teams.forEach((team, index) => {
setTotal(resp.data.total ? resp.data.total : 0);
let newTeams: Team[] = []
resp.data.teams.forEach(team => {
let u = asUser ? asUser : user.id();
let t = Team.create(team)
if (team.settings && u !== "") {
userAPI
.getUser(u, teamScope(team.settings?.key))
.getUser(u, teamScope(team.settings.key))
.then(resp => {
newTeamPerms[index] = resp.permissions;
newTeamPrefs[index] = resp.preferences;
newTeamPerms.set(t.key(), resp.permissions)
newTeamPrefs.set(t.key(), resp.preferences)
})
.finally(() => {
if (newTeamPerms[index]) setTeamPerms([...newTeamPerms]);
if (newTeamPrefs[index]) setTeamPrefs(newTeamPrefs);
setTeamPerms(newTeamPerms);
setTeamPrefs(newTeamPrefs);
});
}
newTeams.push(Team.create(team))
});
setTeams(resp.data.teams);
setTeams(newTeams);
})
.finally(() => {
setLoading(false);
@ -154,25 +110,26 @@ export default function TeamList() {
loadTeams();
}, [user, teamAPI, userAPI, limit, page, search, asUser, loadTeams]);
const drawTeams = () => {
return (
<List>
{teams.map((team, index) => {
let permissions = teamPerms[index];
const columns = (): Column<Team>[] => {
return [
{
title: "",
render: team => {
let permissions = teamPerms.get(team.settings?.key);
if (!permissions) {
permissions = [];
}
let preferences = teamPrefs[index];
let preferences = teamPrefs.get(team.id());
if (!preferences) {
preferences = pond.TeamPreferences.create();
}
return (
<React.Fragment key={index}>
<ListItem
alignItems="flex-start"
disablePadding={!isMobile}
secondaryAction={
<TeamActions
team={Team.create(team)}
team={team}
permissions={permissions}
refreshCallback={loadTeams}
removeSelfCallback={loadTeams}
@ -180,11 +137,14 @@ export default function TeamList() {
toggleNotificationPreference={() => {
if (preferences) {
let newTeamPrefs = cloneDeep(teamPrefs);
newTeamPrefs[index].notify = !newTeamPrefs[index].notify;
let prefs = newTeamPrefs.get(team.id()) ?? pond.TeamPreferences.create()
console.log(prefs)
prefs.notify = !newTeamPrefs.get(team.id())?.notify
newTeamPrefs.set(team.id(), prefs)
teamAPI
.updatePreferences(
Team.create(team).key(),
newTeamPrefs[index],
team.key(),
prefs,
getContextKeys(),
getContextTypes()
)
@ -196,7 +156,12 @@ export default function TeamList() {
/>
}
>
<ListItemAvatar>
<ListItemButton
onClick={() => {
if (team.settings) history(team.settings.key);
}}
>
<ListItemAvatar sx={{ marginLeft: isMobile ? -2 : 0 }}>
<Avatar
onClick={() => {
if (team.settings) history(team.settings.key);
@ -208,152 +173,73 @@ export default function TeamList() {
</ListItemAvatar>
<ListItemText
primary={
<div
className={classes.clickable}
onClick={() => {
if (team.settings) history(team.settings.key);
}}>
<Typography fontWeight={"bold"}>
{team.settings?.name}
</div>
</Typography>
}
secondary={
<React.Fragment>
<Typography component="span" variant="body2" color="textPrimary">
!isMobile && <React.Fragment>
<Typography component="span" variant="body2" color="textSecondary">
{team.settings?.info}
</Typography>
</React.Fragment>
}
/>
</ListItemButton>
</ListItem>
<Divider variant="inset" component="li" />
</React.Fragment>
);
})}
</List>
);
};
const showProgress = () => {
return (
<div
style={{
width: "100%",
textAlign: "center"
}}>
<CircularProgress
style={{
textAlign: "center",
marginTop: theme.spacing(12),
marginBottom: theme.spacing(12)
}}
/>
</div>
);
};
const firstPage = () => {
setPage(0);
};
const lastPage = () => {
setPage(Math.round(total / limit));
};
const nextPage = () => {
if (limit * (page + 1) < total) setPage(page + 1);
};
const prevPage = () => {
if (page > 0) setPage(page - 1);
};
)
}
}
]
}
const handleChange = (event: any) => {
setLimit(event.target.value);
};
const showPageButtons = () => {
const renderGutter = (team: Team) => {
return(
<div style={{ display: "flex", float: "right", alignItems: "center" }}>
<Typography style={{ marginRight: theme.spacing(2) }}>Teams per page:</Typography>
<TextField
select
value={limit}
onChange={handleChange}
style={{ marginRight: theme.spacing(1) }}>
{limits.map(option => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<IconButton disabled={page < 1} size="small" color="inherit" onClick={firstPage}>
<FirstIcon />
</IconButton>
<IconButton disabled={page < 1} size="small" color="inherit" onClick={prevPage}>
<PrevIcon />
</IconButton>
<Typography
variant="subtitle1"
style={{ marginRight: theme.spacing(1), marginLeft: theme.spacing(1) }}>
{limit * page + 1} {limit * page + limit <= limit ? limit * page + limit : limit + 1} of{" "}
{total}
<Typography variant="body2" color="textSecondary" sx={{ padding: 1 }}>
{team.settings.info}
</Typography>
<IconButton
disabled={limit * (page + 1) >= total}
size="small"
color="inherit"
onClick={nextPage}>
<NextIcon />
</IconButton>
<IconButton
disabled={limit * (page + 1) >= total}
size="small"
color="inherit"
onClick={lastPage}>
<LastIcon />
</IconButton>
</div>
);
};
)
}
const renderTitle = () => {
return (
<Card className={classes.cardContent}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography
variant={"h6"}
style={{
marginLeft: theme.spacing(1),
marginTop: "auto",
marginBottom: "auto",
marginRight: theme.spacing(2)
}}>
{isUser ? user.name() + "'s " : "Someone's "} Teams
<Box className={classes.titleBox}>
<Typography variant="h5">
Teams
</Typography>
<IconButton onClick={() => setTeamDialogOpen(true)} color="primary" className={classes.button}>
<AddBox/>
</IconButton>
<TextField
className={classes.input}
value={search}
onChange={event => setSearch(event?.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
</Box>
)
}}></TextField>
</div>
<Typography variant={"subtitle2"} style={{ marginLeft: theme.spacing(2) }}>
{isUser ? "Viewing all teams that you are a part of" : "These are someone else's teams"}
</Typography>
{!loading ? drawTeams() : showProgress()}
{showPageButtons()}
}
return (
<>
<ResponsiveTable<Team>
title={renderTitle()}
rows={teams}
columns={columns()}
handleRowsPerPageChange={handleChange}
total={total}
pageSize={limit}
page={page}
setPage={setPage}
setSearchText={setSearch}
isLoading={loading}
renderGutter={isMobile ? renderGutter : undefined}
hideKeys
/>
<TeamSettings
teamDialogOpen={teamDialogOpen}
closeTeamDialogCallback={() => setTeamDialogOpen(false)}
refreshCallback={loadTeams}
/>
</Card>
);
</>
)
}

View file

@ -247,13 +247,15 @@ export default function TeamSettings(props: Props) {
</DeleteButton>
)}
<Button onClick={close} color="primary" aria-label="Cancel">
<Typography variant="subtitle2" color="textPrimary">
Cancel
</Typography>
</Button>
<Button
disabled={disable}
onClick={teamKey ? updateTeam : addTeam}
style={{ marginRight: theme.spacing(2) }}>
<Typography variant="subtitle2" color="textPrimary">
<Typography variant="subtitle2" color="primary">
{teamKey ? "Update" : "Submit"}
</Typography>
</Button>