teams list now renders

This commit is contained in:
Carter 2024-11-18 10:10:29 -06:00
parent 17c559bdfc
commit e95b654d7f
81 changed files with 6132 additions and 40 deletions

197
src/teams/TeamActions.tsx Normal file
View file

@ -0,0 +1,197 @@
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip
} from "@mui/material";
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
import MoreIcon from "@mui/icons-material/MoreVert";
import GroupSettingsIcon from "@mui/icons-material/Settings";
import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import { Team, teamScope } from "models";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
import ObjectUsers from "user/ObjectUsers";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import TeamSettings from "./TeamSettings";
import NotificationButton from "common/NotificationButton";
import { blue } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles(() => ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
}
}));
interface Props {
team: Team;
permissions: pond.Permission[];
refreshCallback: () => void;
preferences: pond.TeamPreferences;
toggleNotificationPreference: () => void;
}
interface OpenState {
share: boolean;
users: boolean;
teams: boolean;
settings: boolean;
removeSelf: boolean;
}
export default function TeamActions(props: Props) {
const classes = useStyles();
const { team, permissions, refreshCallback, preferences, toggleNotificationPreference } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [openState, setOpenState] = useState<OpenState>({
share: false,
users: false,
teams: false,
settings: false,
removeSelf: false
});
const groupMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{!isOffline() && canShare && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, share: true });
setAnchorEl(null);
}}
// button
>
<ListItemIcon>
<ShareObjectIcon className={classes.shareIcon} />
</ListItemIcon>
<ListItemText primary="Share" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, users: true });
setAnchorEl(null);
}}
// button
>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, removeSelf: true });
setAnchorEl(null);
}}
// button
>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const dialogs = () => {
const key = team.key();
const label = team.name();
return (
<React.Fragment>
<TeamSettings
team={team}
permissions={permissions}
teamDialogOpen={openState.settings}
closeTeamDialogCallback={() => {
setOpenState({ ...openState, settings: false });
}}
refreshCallback={refreshCallback}
/>
<ShareObject
scope={teamScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.share}
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
/>
<ObjectUsers
scope={teamScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.users}
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
<RemoveSelfFromObject
scope={teamScope(key)}
label={label}
isDialogOpen={openState.removeSelf}
closeDialogCallback={() => setOpenState({ ...openState, removeSelf: false })}
/>
</React.Fragment>
);
};
const canWrite = permissions.includes(pond.Permission.PERMISSION_WRITE);
return (
<React.Fragment>
{/* <NotificationButton
notify={preferences.notify}
onChange={toggleNotificationPreference}
tooltip={
"Notifications for " +
team.name() +
" are " +
(preferences.notify ? "enabled" : "disabled")
}
// hidden={isLoading}
/> */}
{canWrite && (
<Tooltip title="Team Settings">
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
<GroupSettingsIcon />
</IconButton>
</Tooltip>
)}
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
{groupMenu()}
{dialogs()}
</React.Fragment>
);
}

360
src/teams/TeamList.tsx Normal file
View file

@ -0,0 +1,360 @@
import React, { useCallback, useEffect, useState } from "react";
import {
Card,
Theme,
List,
ListItem,
ListItemAvatar,
Avatar,
ListItemText,
Typography,
ListItemSecondaryAction,
Divider,
useTheme,
TextField,
InputAdornment,
CircularProgress,
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 { Add } from "@mui/icons-material";
import { cloneDeep } from "lodash";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { makeStyles, styled } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
cardContent: {
padding: theme.spacing(1)
},
clickable: {
"&:hover": {
textDecoration: "underline",
cursor: "pointer"
}
},
input: {
marginLeft: "auto",
marginRight: theme.spacing(2),
marginTop: theme.spacing(1)
}
}))
const limits = [
{
value: 5,
label: "5"
},
{
value: 10,
label: "10"
},
{
value: 15,
label: "15"
},
{
value: 20,
label: "20"
}
];
interface Props {}
export default function TeamList(props: Props) {
const classes = useStyles();
const [teams, setTeams] = useState<pond.Team[]>([]);
const teamAPI = useTeamAPI();
const userAPI = useUserAPI();
const history = useNavigate();
const theme = useTheme();
const [{ user, as }] = useGlobalState();
const [teamPerms, setTeamPerms] = useState<pond.Permission[][]>([]);
const [teamPrefs, setTeamPrefs] = useState<pond.TeamPreferences[]>([]);
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 ColorAdd = styled(Add)((theme: Theme) => ({
// color: theme.palette.getContrastText(theme.palette.primary.main),
// backgroundColor: theme.palette.primary.main,
// marginTop: 'auto',
// marginBottom: 'auto',
// borderRadius: '4px',
// '&:hover': {
// backgroundColor: theme.palette.primary.light,
// cursor: 'pointer',
// },
// '&:disabled': {
// backgroundColor: 'rgba(0,0,0,0)',
// color: theme.palette.getContrastText(theme.palette.primary.main),
// },
// }));
useEffect(() => {
if (as.includes("auth0")) setAsUser(as);
}, [as]);
useEffect(() => {
setUserId(user.id());
}, [user]);
useEffect(() => {
if (!asUser) {
setIsUser(true);
return;
}
setIsUser(userId === asUser);
}, [userId, asUser]);
const loadTeams = useCallback(() => {
setLoading(true);
let newTeamPerms: pond.Permission[][] = [];
let newTeamPrefs: pond.TeamPreferences[] = [];
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) => {
let u = asUser ? asUser : user.id();
if (team.settings && u !== "") {
userAPI
.getUser(u, teamScope(team.settings?.key))
.then(resp => {
newTeamPerms[index] = resp.permissions;
newTeamPrefs[index] = resp.preferences;
})
.finally(() => {
if (newTeamPerms[index]) setTeamPerms([...newTeamPerms]);
if (newTeamPrefs[index]) setTeamPrefs(newTeamPrefs);
});
}
});
setTeams(resp.data.teams);
})
.finally(() => {
setLoading(false);
});
}, [user, teamAPI, userAPI, limit, page, search, asUser]);
useEffect(() => {
loadTeams();
}, [user, teamAPI, userAPI, limit, page, search, asUser, loadTeams]);
const drawTeams = () => {
return (
<List>
{teams.map((team, index) => {
let permissions = teamPerms[index];
if (!permissions) {
permissions = [];
}
let preferences = teamPrefs[index];
if (!preferences) {
preferences = pond.TeamPreferences.create();
}
return (
<React.Fragment key={index}>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar
onClick={() => {
if (team.settings) history("teams/" + team.settings.key);
}}
src={team.settings?.avatar}
alt=""
className={classes.clickable}
/>
</ListItemAvatar>
<ListItemText
primary={
<div
className={classes.clickable}
onClick={() => {
if (team.settings) history("teams/" + team.settings.key);
}}>
{team.settings?.name}
</div>
}
secondary={
<React.Fragment>
<Typography component="span" variant="body2" color="textPrimary">
{team.settings?.info}
</Typography>
</React.Fragment>
}
/>
{/* <ListItemSecondaryAction>
<TeamActions
team={Team.create(team)}
permissions={permissions}
refreshCallback={loadTeams}
preferences={preferences}
toggleNotificationPreference={() => {
if (preferences) {
let newTeamPrefs = cloneDeep(teamPrefs);
newTeamPrefs[index].notify = !newTeamPrefs[index].notify;
console.log(newTeamPrefs[index]);
teamAPI
.updatePreferences(
Team.create(team).key(),
newTeamPrefs[index],
getContextKeys(),
getContextTypes()
)
.then(resp => {
setTeamPrefs(newTeamPrefs);
});
// setTeamPrefs(newTeamPrefs)
}
}}
/>
</ListItemSecondaryAction> */}
</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 = () => {
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>
<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>
);
};
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
</Typography>
<Add onClick={() => setTeamDialogOpen(true)} />
<TextField
className={classes.input}
value={search}
onChange={event => setSearch(event?.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}></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()}
<TeamSettings
teamDialogOpen={teamDialogOpen}
closeTeamDialogCallback={() => setTeamDialogOpen(false)}
refreshCallback={loadTeams}
/>
</Card>
);
}

265
src/teams/TeamSettings.tsx Normal file
View file

@ -0,0 +1,265 @@
import {
Box,
Button,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Tab,
Tabs,
TextField,
Typography,
useTheme,
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Team } from "models";
import { pond } from "protobuf-ts/pond";
import { useSnackbar, useTeamAPI } from "providers";
import React, { useEffect, useState } from "react";
import DeleteButton from "common/DeleteButton";
import { userRoleFromPermissions } from "pbHelpers/User";
import { IconPicker } from "common/IconPicker";
interface Props {
teamDialogOpen: boolean;
closeTeamDialogCallback: () => void;
refreshCallback?: () => void;
team?: Team;
permissions?: pond.Permission[];
}
export default function TeamSettings(props: Props) {
const theme = useTheme();
const teamAPI = useTeamAPI();
const { teamDialogOpen, closeTeamDialogCallback, permissions, refreshCallback } = props;
const [loading, setLoading] = useState<boolean>(false);
const [nameField, setNameField] = useState<string>("");
const [infoField, setInfoField] = useState<string>("");
const [url, setUrl] = useState<string>("");
const snackbar = useSnackbar();
const [tab, setTab] = useState(0);
const [team, setTeam] = useState<Team>();
const [teamKey, setTeamKey] = useState<string>();
const [openDelete, setOpenDelete] = useState(false);
useEffect(() => {
if (props.team) {
setTeam(props.team);
setTeamKey(props.team.key());
}
}, [props.team]);
useEffect(() => {
if (team) {
setNameField(team.name());
setInfoField(team.settings.info);
setUrl(team.settings.avatar);
}
}, [team]);
// const ColorButton = withStyles((theme: Theme) => ({
// root: {
// color: theme.palette.getContrastText(theme.palette.primary.main),
// backgroundColor: theme.palette.primary.main,
// "&:hover": {
// backgroundColor: theme.palette.primary.light
// },
// "&:disabled": {
// backgroundColor: "rgba(0,0,0,0)",
// color: theme.palette.getContrastText(theme.palette.primary.main)
// }
// }
// }))(Button);
const removeTeam = () => {
if (team?.key())
teamAPI
.removeTeam(team?.key())
.then(() => {
snackbar.info("Team removed successfully.");
if (refreshCallback) refreshCallback();
closeTeamDialogCallback();
})
.catch(err => {
console.log(err);
});
};
const addTeam = () => {
let team = pond.TeamSettings.create();
team.name = nameField;
team.info = infoField;
setLoading(true);
teamAPI
.addTeam(team)
.then(resp => {
snackbar.success("Team added successfully!");
setTab(1);
let t = pond.Team.create();
t.settings = team;
setTeam(Team.create(t));
setTeamKey(resp.data.team);
if (refreshCallback) refreshCallback();
})
.catch(err => {
snackbar.error(err);
})
.finally(() => {
setLoading(false);
});
};
const updateTeam = () => {
let newTeam = pond.TeamSettings.create();
newTeam.name = nameField;
newTeam.info = infoField;
newTeam.avatar = url;
if (teamKey) {
teamAPI
.updateTeam(teamKey, newTeam)
.then(() => {
snackbar.success("Team update successfully!");
})
.catch(err => {
snackbar.error(err);
})
.finally(() => {
if (refreshCallback) refreshCallback();
setNameField("");
setInfoField("");
setUrl("");
setTeamKey(undefined);
setTab(0);
closeTeamDialogCallback();
});
}
};
const disableButton = () => {
if (loading) return true;
if (nameField === "" || infoField === "") return true;
};
const close = () => {
setNameField("");
setInfoField("");
setUrl("");
closeTeamDialogCallback();
};
const isOwner = () => {
if (permissions) {
return userRoleFromPermissions(permissions) === "Owner";
}
return false;
};
const deleteConfirmation = () => {
return (
<ResponsiveDialog
open={openDelete}
onClose={() => {
setOpenDelete(false);
}}>
<DialogTitle>Delete Team</DialogTitle>
<DialogContent>
<Typography>
Performing this action will delete this team and and any file attachments associated
with it.
</Typography>
<Typography>Are you sure you wish to continue.</Typography>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDelete(false);
}}>
Close
</Button>
<DeleteButton onClick={removeTeam}>Confirm</DeleteButton>
</DialogActions>
</ResponsiveDialog>
);
};
const teamSettings = () => {
return (
<DialogContent>
<DialogContentText style={{ margin: theme.spacing(1) }}>
Add more info about your team, business, or company. You can have multiple teams if
needed.
</DialogContentText>
<TextField
label="Name"
value={nameField}
onChange={event => setNameField(event?.target.value)}
variant="outlined"
style={{ margin: theme.spacing(1) }}
fullWidth
/>
<div style={{ marginTop: theme.spacing(1) }} />
<TextField
label="Info"
value={infoField}
onChange={event => setInfoField(event?.target.value)}
variant="outlined"
style={{ margin: theme.spacing(1) }}
fullWidth
/>
</DialogContent>
);
};
let disable = disableButton();
return (
<React.Fragment>
<ResponsiveDialog open={teamDialogOpen} onClose={close}>
<DialogTitle>{team ? "Update " + team.name() + "?" : "Create New Team?"}</DialogTitle>
<Box style={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={tab}
onChange={(_, value) => setTab(value)}
aria-label="basic tabs example"
centered>
<Tab label="Name & Info" key={1} />
<Tab
label="Team Icon"
key={2}
disabled={team?.key() === undefined || team?.key() === ""}
/>
</Tabs>
</Box>
{tab === 0 && teamSettings()}
{tab === 1 && teamKey !== undefined && (
<DialogContent>
<IconPicker setUrl={setUrl} url={url} id={teamKey} />
</DialogContent>
)}
<DialogActions>
{isOwner() && (
<DeleteButton
onClick={() => {
setOpenDelete(true);
}}
aria-label="Cancel"
style={{ marginRight: "auto", marginLeft: theme.spacing(2) }}>
Delete
</DeleteButton>
)}
<Button onClick={close} color="primary" aria-label="Cancel">
Cancel
</Button>
<Button
disabled={disable}
onClick={teamKey ? updateTeam : addTeam}
style={{ marginRight: theme.spacing(2) }}>
<Typography variant="subtitle2" color="textPrimary">
{teamKey ? "Update" : "Submit"}
</Typography>
</Button>
</DialogActions>
</ResponsiveDialog>
{deleteConfirmation()}
</React.Fragment>
);
}