diff --git a/src/bin/BinYards.tsx b/src/bin/BinYards.tsx new file mode 100644 index 0000000..c32572a --- /dev/null +++ b/src/bin/BinYards.tsx @@ -0,0 +1,792 @@ +import { + AppBar, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Tab, + Tabs, + TextField, + Theme, + Tooltip, + Typography, + useTheme +} from "@mui/material"; +import { pond } from "protobuf-ts/pond"; +import { + useBinYardAPI, + useSnackbar, + //useUserAPI, + usePermissionAPI, + useBinAPI, + useGlobalState + //useTeamAPI +} from "providers"; +import React, { useEffect, useState } from "react"; +import AddIcon from "@mui/icons-material/LibraryAdd"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { MoreVert, Search } from "@mui/icons-material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import RemoveSelfIcon from "@mui/icons-material/ExitToApp"; +import KeyboardArrowLeftIcon from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; +import ObjectUsersIcon from "@mui/icons-material/SupervisedUserCircle"; +import EditIcon from "@mui/icons-material/Edit"; +import ShareIcon from "@mui/icons-material/Share"; +import { red, blue, green } from "@mui/material/colors"; +import { openSnackbar } from "providers/Snackbar"; +// import { Status } from "@sentry/react"; +import { binScope, binYardScope } from "models/Scope"; +import ShareObject from "user/ShareObject"; +import { Bin, Team, User } from "models"; +import { Dictionary } from "lodash"; +import ObjectUsers from "user/ObjectUsers"; +import ObjectTeams from "teams/ObjectTeams"; +import { GrainBag } from "models/GrainBag"; +import { makeStyles } from "@mui/styles"; + +const parentTab = { + "&": { + margin: "0px", + marginTop: "4px", + marginLeft: "4px", + animationDuration: "10s", + background: "rgba(150, 150, 150, 0)", + + borderRadius: "-5px", + borderTopLeftRadius: "6px", + borderTopRightRadius: "6px" + }, + "&:hover": { + background: "linear-gradient(rgba(150, 150, 150, 0.2), rgba(150, 150, 150, 0))" + } +}; + +const useStyles = makeStyles((theme: Theme) => { + return ({ + red: { + marginRight: 0, + paddingRight: 0, + color: red["500"], + "&:hover": { + color: red["600"] + } + }, + blue: { + marginRight: 0, + paddingRight: 0, + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + green: { + marginRight: 0, + paddingRight: 0, + color: green["500"], + "&:hover": { + color: green["600"] + } + }, + searchTab: { + ...parentTab, + width: theme.spacing(8), + minWidth: theme.spacing(8), + left: 0, + height: "100%", + background: theme.palette.background.paper + }, + searchTabExpanded: { + ...parentTab, + width: theme.spacing(30), + minWidth: theme.spacing(30), + left: 0, + height: "100%", + background: theme.palette.background.paper + }, + tab: { + ...parentTab, + width: theme.spacing(20), + minWidth: theme.spacing(20), + left: 0, + height: "100%", + background: theme.palette.background.paper + }, + smallTab: { + ...parentTab, + width: theme.spacing(8), + minWidth: theme.spacing(8), + background: theme.palette.background.paper + }, + selectedTab: { + borderRadius: "-5px", + borderTopLeftRadius: "6px", + borderTopRightRadius: "6px", + background: theme.palette.background.default, + "&:hover": { + background: + "linear-gradient(rgba(150, 150, 150, 0.3)," + theme.palette.background.default + " 80%)" + } + }, + icon: { + display: "flex", + position: "absolute", + right: 0, + top: 6, + padding: 6, + marginRight: "2px", + width: 36, + height: 36, + borderRadius: "18px", + background: "rgba(0,0,0,0)", + "&:hover": { + background: + "radial-gradient(closest-side, rgba(150, 150, 150, 0.5) 50%, rgba(150, 150, 150, 0.5))" + } + }, + tabText: { + position: "absolute", + left: theme.spacing(2), + color: "transparent", + textAlign: "left", + width: "80%", + backgroundImage: "linear-gradient(to right, white 70%, rgba(200, 200, 200, 0) 87.5%)", + backgroundClip: "text", + WebkitBackgroundClip: "text", + overflowX: "hidden", + whiteSpace: "nowrap", + top: 12 + } + }); +}); + +interface Props { + setYardFilter: React.Dispatch>; + + loadBins: () => void; + loadYards: (search?: string, limit?: number, offset?: number) => void; + yardsLoading: boolean; + tab?: number; + yards?: pond.BinYardSettings[]; + yardPerms?: Dictionary; + showTabs?: boolean; + bins: Bin[]; + grainBags: GrainBag[]; + setShowing: (showing: "all" | "bins" | "bags") => void; +} + +export default function BinYard(props: Props) { + const { setYardFilter, loadBins, loadYards, grainBags, setShowing, yardsLoading } = props; + const binYardAPI = useBinYardAPI(); + const binAPI = useBinAPI(); + // const userAPI = useUserAPI(); + // const teamAPI = useTeamAPI(); + const [tab, setTab] = useState(props.tab ? props.tab : 1); + const [showAddYard, setShowAddYard] = useState(false); + const [addYardName, setAddYardName] = useState(""); + const [addYardDescription, setAddYardDescription] = useState(""); + const [binYards, setBinYards] = useState(props.yards ? props.yards : []); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [yardIndex, setYardIndex] = useState(0); + const [tabClick, setTabClick] = useState(true); + const [shareDialog, setShareDialog] = useState(false); + const [userDialog, setUserDialog] = useState(false); + const [teamDialog, setTeamDialog] = useState(false); + const [openUpdateYardDialog, setOpenUpdateYardDialog] = useState(false); + const { error, warning, success } = useSnackbar(); + const theme = useTheme(); + const classes = useStyles(); + const [leaveYardDialogOpen, setLeaveYardDialogOpen] = useState(false); + const permissionAPI = usePermissionAPI(); + const [leaving, setLeaving] = useState(false); + const [yardPermissions, setYardPermissions] = useState>({}); + const [searchSelected, setSearchSelected] = useState(false); + const [{ user, as }] = useGlobalState(); + + useEffect(() => { + if (props.yards && props.yardPerms) { + setBinYards(props.yards); + setYardPermissions(props.yardPerms); + return; + } + }, [props.yards, props.yardPerms]); + + // useEffect(() => { + // if (props.tab && binYards.length > 1) { + // setYardFilter(binYards[props.tab - 1].key); + // } + // }, [props.tab, binYards, setYardFilter, loadYards]); + + const tabChange = (i: number) => { + if (tab === i) return; + if (i === binYards.length + 2) return; + if (tabClick !== true) return; + setTab(i); + setYardFilter(""); + if (i > 1 && i <= binYards.length + 1) { + setYardFilter(binYards[i - 2].key); + } + }; + + const submitYard = () => { + let newBinYard = pond.BinYardSettings.create(); + newBinYard.name = addYardName; + newBinYard.description = addYardDescription; + newBinYard.owner = user.id(); + newBinYard.userSort[user.id()] = binYards.length + 1; + binYardAPI + .addBinYard(newBinYard) + .then(_resp => { + loadYards(); + setShowAddYard(false); + openSnackbar("success", "Bin Yard created :)"); + }) + .catch(_err => { + error("Could not add bin yard"); + }); + }; + + const updateYard = () => { + let newBinYard = binYards[yardIndex]; + newBinYard.name = addYardName; + newBinYard.description = addYardDescription; + binYardAPI + .updateBinYard(newBinYard.key, newBinYard) + .then(_resp => { + loadYards(); + openSnackbar("success", "Bin Yard updated"); + setOpenUpdateYardDialog(false); + }) + .catch(_err => { + error("Could not update bin yard"); + setOpenUpdateYardDialog(false); + }); + }; + + const yardForm = () => { + return ( + + + Bin yards allow you to organise your bins by tab. Bins created while a bin yard tab is + opened will automatically be added to that bin yard. You may also choose a yard for a bin + in the bin settings page. + + setAddYardName(event.target.value)} + style={{ marginTop: theme.spacing(0) }} + fullWidth + variant="outlined" + /> + + Bin yard description (optional): + + setAddYardDescription(event.target.value)} + style={{ marginTop: theme.spacing(0) }} + fullWidth + variant="outlined" + /> + + ); + }; + + const addYardDialog = () => { + return ( + setShowAddYard(false)}> + Create New Bin Yard + {yardForm()} + + + + + + ); + }; + + const updateYardDialog = () => { + return ( + setOpenUpdateYardDialog(false)}> + Update Bin Yard + {yardForm()} + + + + + + ); + }; + + const deleteYard = (index: number) => { + let newBinYards = binYards; + binYardAPI + .removeBinYard(binYards[index].key) + .then(resp => { + newBinYards.splice(index, 1); + if (index + 1 === tab) { + tabChange(0); + } else if (index + 1 < tab) { + tabChange(tab - 1); + } + setBinYards([...newBinYards]); + }) + .catch(err => { + error("Failed to delete bin yard"); + }); + }; + + const moveLeft = (index: number) => { + let key = as ? as : user.id(); + if (binYards[index - 1]) { + let left = binYards[index - 1].userSort[key] ?? index - 1; + let right = binYards[index].userSort[key] ?? index; + let binLeft = binYards[index - 1]; + let binRight = binYards[index]; + binLeft.userSort[key] = right; + binRight.userSort[key] = left; + if (binRight.userSort[key] >= binLeft.userSort[key]) { + binLeft.userSort[key] = binRight.userSort[key] + 1; + } + let leftPromise = binYardAPI.updateBinYard(binLeft.key, binLeft, true); + let rightPromise = binYardAPI.updateBinYard(binRight.key, binRight, true); + Promise.all([leftPromise, rightPromise]).then((/*[leftResp, rightResp]*/) => { + loadYards(); + if (index === tab - 1) setTab(tab - 1); + if (index === tab) setTab(tab + 1); + }); + } + }; + + const moveRight = (index: number) => { + let key = as ? as : user.id(); + if (binYards[index + 1]) { + let left = binYards[index].userSort[key] ?? index; + let right = binYards[index + 1].userSort[key] ?? index + 1; + let binLeft = binYards[index]; + let binRight = binYards[index + 1]; + binLeft.userSort[key] = right; + binRight.userSort[key] = left; + if (binRight.userSort[key] <= binLeft.userSort[key]) { + binRight.userSort[key] = binLeft.userSort[key] - 1; + } + let leftPromise = binYardAPI.updateBinYard(binLeft.key, binLeft, true); + let rightPromise = binYardAPI.updateBinYard(binRight.key, binRight, true); + Promise.all([leftPromise, rightPromise]).then((/*[leftResp, rightResp]*/) => { + loadYards(); + if (index === tab - 1) setTab(tab + 1); + if (index === tab - 2) setTab(tab - 1); + }); + } + }; + + const openShareDialog = () => { + setShareDialog(true); + }; + + const leaveYardDialog = () => { + if (!binYards[yardIndex]) return; + let label = binYards[yardIndex].name; + return ( + setLeaveYardDialogOpen(false)} + aria-labelledby="remove-self-from-object"> + {"Leave " + label + "?"} + + + You will no longer be able to access {label}. You will also lose access to all bins in{" "} + {label} that you do not own. + + + + + + + + ); + }; + + const leaveYard = () => { + updateYardPermissions(); + }; + + const updateYardPermissions = (users?: User[] | Team[]) => { + if (leaving) return; + setLeaving(true); + let binSuccess = true; + setLeaving(true); + binAPI.listBins(150, 0, "asc", "name", binYards[yardIndex].key).then(resp => { + resp.data.bins.forEach(bin => { + if (bin.settings) { + let scope = binScope(bin.settings?.key); + users = users + ? users + : [ + User.create( + pond.User.create({ + settings: pond.UserSettings.create({ + id: user.id() + }), + permissions: [] + }) + ) + ]; + permissionAPI.updatePermissions(scope, users).catch(_ => { + binSuccess = false; + }); + } + }); + }); + if (binSuccess) { + let scope = binYardScope(binYards[yardIndex].key); + let label = binYards[yardIndex].name; + users = users + ? users + : [ + User.create( + pond.User.create({ + settings: pond.UserSettings.create({ + id: user.id() + }), + permissions: [] + }) + ) + ]; + permissionAPI + .updatePermissions(scope, users) + .then((response: any) => { + success("Successfully updated permissions for " + label); + setLeaveYardDialogOpen(false); + loadYards(); + loadBins(); + tabChange(0); + }) + .catch((err: any) => { + err.response.data.error + ? warning(err.response.data.error) + : error("Error occured when updating permissions for " + label); + setLeaveYardDialogOpen(false); + }) + .finally(() => { + setLeaving(false); + }); + } else { + error("Error updating permissions on one or more bins"); + setLeaving(false); + } + }; + + const yardMenu = () => { + let permissions: pond.Permission[] = []; + if (binYards[yardIndex] === undefined) return; + if (yardPermissions[binYards[yardIndex].key] !== undefined) { + permissions = yardPermissions[binYards[yardIndex].key]; + } + return ( + { + setMenuAnchorEl(null); + }} + disableAutoFocusItem> + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + { + setOpenUpdateYardDialog(true); + setAddYardName(binYards[yardIndex].name); + setAddYardDescription(binYards[yardIndex].description); + setMenuAnchorEl(null); + }} + aria-label="Edit Yard" + dense> + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_SHARE) && ( + { + setMenuAnchorEl(null); + openShareDialog(); + }} + aria-label="Share Yard" + dense> + + + + + + )} + { + moveLeft(yardIndex); + setMenuAnchorEl(null); + }} + aria-label="Move Left" + dense> + + + + + + { + moveRight(yardIndex); + setMenuAnchorEl(null); + }} + aria-label="Move Right" + dense> + + + + + + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + { + setUserDialog(true); + setMenuAnchorEl(null); + }} + aria-label="Users" + dense> + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + { + setTeamDialog(true); + setMenuAnchorEl(null); + }} + aria-label="Teams" + dense> + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_SHARE) && + permissions.includes(pond.Permission.PERMISSION_READ) && + permissions.includes(pond.Permission.PERMISSION_USERS) && + permissions.includes(pond.Permission.PERMISSION_WRITE) ? ( + { + deleteYard(yardIndex); + setMenuAnchorEl(null); + }} + aria-label="Delete Yard" + dense> + + + + + + ) : ( + { + setLeaveYardDialogOpen(true); + setMenuAnchorEl(null); + }} + aria-label="Leave Yard" + dense> + + + + + + )} + + ); + }; + + let permissions: pond.Permission[] = []; + if (binYards[yardIndex] !== undefined) { + permissions = yardPermissions[binYards[yardIndex].key] || []; + } + return ( + + {addYardDialog()} + {updateYardDialog()} + {yardMenu()} + {leaveYardDialog()} + {binYards[yardIndex] && ( + setShareDialog(false)} + /> + )} + {binYards[yardIndex] && ( + setUserDialog(false)} + refreshCallback={() => {}} + userCallback={updateYardPermissions} + dialog={ + "Updating a user's permissions for this bin yard will update the same permissions for all corresponding bins." + } + /> + )} + {binYards[yardIndex] && ( + setTeamDialog(false)} + refreshCallback={() => {}} + userCallback={updateYardPermissions} + dialog={ + "Updating a user's permissions for this bin yard will update the same permissions for all corresponding bins." + } + /> + )} + { + if (+newValue > 0) { + tabChange(newValue); + } + }}> + + {yardsLoading ? : } + {searchSelected && ( + + { + loadYards(e.target.value); + }} + /> + + )} + + } + onClick={e => { + setSearchSelected(true); + }} + /> + { + setShowing("all"); + }} + /> + {binYards.map((n, index) => ( + +
{n.name}
+
{n.description}
+ + ) : ( + n.name + ) + } + key={index}> + { + setShowing("bins"); + }} + label={ + +
{n.name}
+ setTabClick(false)} + onMouseLeave={() => setTabClick(true)} + className={classes.icon} + onClick={event => { + let target = event.currentTarget; + setMenuAnchorEl(target); + setYardIndex(index); + }} + /> +
+ } + /> +
+ ))} + } + onClick={() => setShowAddYard(true)} + disableTouchRipple={true} + /> + {/* {grainBags.length > 0 && ( + { + setShowing("bags"); + }} + /> + )} */} +
+
+ ); +} diff --git a/src/common/ImgIcon.tsx b/src/common/ImgIcon.tsx index c5ebb3d..638cb95 100644 --- a/src/common/ImgIcon.tsx +++ b/src/common/ImgIcon.tsx @@ -42,6 +42,7 @@ export function ImgIcon(props: Props) { src={src} color="transparent" style={style} + loading="lazy" /> ); diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 8975cef..7917111 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -81,6 +81,7 @@ import { GrainBag } from "models/GrainBag"; import { getThemeType } from "theme/themeType"; import GrainBagSettings from "grainBag/grainBagSettings"; import GrainBagList from "grainBag/grainBagList"; +import BinYards from "bin/BinYards"; // import { useHistory } from "react-router"; const useStyles = makeStyles((theme: Theme) => { @@ -1315,7 +1316,7 @@ export default function Bins(props: Props) { return ( {addALertsDialog()} - {/* { setShowing(val); }} - /> */} + /> { if (bin.settings) { let newScope = binScope(bin.settings?.key); - permissionAPI.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(err => { + permissionAPI.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(_err => { successBins = false; }); } @@ -143,7 +143,7 @@ export default function ShareObject(props: Props) { resp.data.gates.forEach(gate => { if (gate) { let newScope = { key: gate.key, kind: "gate" } as Scope; - permissionAPI.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(err => { + permissionAPI.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(_err => { successGates = false; }); } @@ -205,7 +205,7 @@ export default function ShareObject(props: Props) { ); setShareableLinks(rShareableLinks); }) - .catch((error: any) => { + .catch((_error: any) => { setShareableLinks([]); console.log("Error occured while loading shareable links"); }) @@ -228,10 +228,10 @@ export default function ShareObject(props: Props) { const expiration = isNewLinkInfinite ? getNeverExpires() : newLinkExpiration.toISOString(); permissionAPI .addShareableLink(scope, expiration) - .then((response: any) => { + .then((_response: any) => { loadShareableLinks(); }) - .catch((error: any) => { + .catch((_error: any) => { console.log("Error occured while creating shareable link"); }); }; @@ -239,10 +239,10 @@ export default function ShareObject(props: Props) { const revokeShareableLink = (code: string) => { permissionAPI .removeShareableLink(scope, code) - .then((response: any) => { + .then((_response: any) => { loadShareableLinks(); }) - .catch((error: any) => { + .catch((_error: any) => { console.log("Error occured while removing the shareable link"); }); }; @@ -297,7 +297,7 @@ export default function ShareObject(props: Props) { setSharedPermissions(updatedSharedPermissions); }; - const changeTab = (event: any, newTab: number) => { + const changeTab = (_event: any, newTab: number) => { setTab(newTab); }; diff --git a/src/ventilation/Editor.tsx b/src/ventilation/Editor.tsx index efce341..caa3e6e 100644 --- a/src/ventilation/Editor.tsx +++ b/src/ventilation/Editor.tsx @@ -324,7 +324,7 @@ export default function Editor(props: Props) { setMouseDown(true); }; - const mouseUp = (e: any) => { + const mouseUp = (_e: any) => { setMouseDown(false); if (mode === Mode.NORMAL && selected !== undefined) { let state = [...moveOrigin]; @@ -522,7 +522,7 @@ export default function Editor(props: Props) { }; const drawBlocks = (context: CanvasRenderingContext2D) => { - blocks.forEach((block, index) => { + blocks.forEach(block => { if (block.draw) block.draw(context, cameraOffsetX, cameraOffsetY, scale); }); selected.forEach(block => {