device settings and actions made functional

This commit is contained in:
Carter 2025-01-10 14:42:30 -06:00
parent 3ce00facd1
commit 4e7c68401b
19 changed files with 3042 additions and 41 deletions

617
src/teams/ObjectTeams.tsx Normal file
View file

@ -0,0 +1,617 @@
import {
Avatar,
Button,
Checkbox,
CircularProgress,
DialogActions,
DialogContent,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid2 as Grid,
IconButton,
Link,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
PaletteColor,
Theme,
Tooltip,
Typography,
useTheme
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { usePermissionAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep, isEqual } from "lodash";
import { Scope, Team, User } from "models";
import { userRoleFromPermissions } from "pbHelpers/User";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { or } from "utils/types";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import { useGlobalState, useTeamAPI } from "providers";
import ShareWithTeam from "./ShareWithTeam";
import { blue, red } from "@mui/material/colors";
import { useNavigate } from "react-router-dom";
import { makeStyles } from "@mui/styles";
import { Share, RemoveCircle as RemoveUserIcon } from "@mui/icons-material";
const useStyles = makeStyles((theme: Theme) => {
const avatarBG = theme.palette.secondary["700" as keyof PaletteColor];
return ({
dialogContent: {
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
[theme.breakpoints.up("sm")]: {
padding: `${theme.spacing(1)}px ${theme.spacing(3)}px`
}
},
userAvatar: {
color: "#fff",
backgroundColor: avatarBG
},
userPermissions: {
paddingLeft: theme.spacing(4),
paddingBottom: theme.spacing(1)
},
textOverflowEllipsis: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
},
removeIcon: {
color: red["500"],
"&:hover": {
color: red["600"]
}
},
verticalSpacing: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(3)
},
lessSpacing: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1)
},
iconButton: {
margin: theme.spacing(1)
},
shareIcon: {
color: blue["600"],
"&:hover": {
color: blue["700"]
}
}
});
});
interface Props {
scope: Scope;
label: string;
permissions: pond.Permission[];
isDialogOpen: boolean;
closeDialogCallback: Function;
refreshCallback: Function;
userCallback?: (users?: User[] | Team[] | undefined) => void;
dialog?: string;
cardMode?: boolean;
}
export default function ObjectTeams(props: Props) {
const navigate = useNavigate();
const classes = useStyles();
const theme = useTheme();
const permissionAPI = usePermissionAPI();
const [{ user, as }] = useGlobalState();
const canProvision = user.allowedTo("provision");
const teamAPI = useTeamAPI();
const { error, success, warning } = useSnackbar();
const {
scope,
label,
permissions,
isDialogOpen,
closeDialogCallback,
refreshCallback,
userCallback,
dialog,
cardMode
} = props;
const prevPermissions = usePrevious(permissions);
const prevIsDialogOpen = usePrevious(isDialogOpen);
const [initialUsers, setInitialUsers] = useState<Team[]>([]);
const [users, setUsers] = useState<Team[]>([]);
const [canManageUsers, setCanManageUsers] = useState<boolean>(
permissions.includes(pond.Permission.PERMISSION_USERS)
);
const [canShare, setCanShare] = useState<boolean>(
permissions.includes(pond.Permission.PERMISSION_SHARE)
);
const [removedUsers, setRemovedUsers] = useState<string[]>([]);
const [removeSelfDialogIsOpen, setRemoveSelfDialogIsOpen] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isShareObjectDialogOpen, setIsShareObjectDialogOpen] = useState<boolean>(false);
const setDefaultState = () => {
setInitialUsers([]);
setUsers([]);
setRemovedUsers([]);
};
const load = useCallback(() => {
setIsLoading(true);
teamAPI
.listObjectTeams(scope, as)
.then((response: any) => {
let rTeams: Team[] = [];
or(response.data, { teams: [] }).teams.forEach((user: any) => {
rTeams.push(Team.any(user));
});
rTeams = rTeams.filter(u => u.permissions.length > 0);
setInitialUsers(cloneDeep(rTeams));
setUsers(rTeams);
})
.catch((_err: any) => {
setInitialUsers([]);
setUsers([]);
})
.finally(() => setIsLoading(false));
}, [scope, teamAPI, as]);
useEffect(() => {
if (!prevIsDialogOpen && isDialogOpen) {
load();
}
if (prevPermissions !== permissions) {
setCanManageUsers(permissions.includes(pond.Permission.PERMISSION_USERS));
setCanShare(permissions.includes(pond.Permission.PERMISSION_SHARE));
}
}, [isDialogOpen, load, permissions, prevIsDialogOpen, prevPermissions]);
const closeRemoveSelfDialog = () => {
setRemoveSelfDialogIsOpen(false);
};
const close = () => {
closeRemoveSelfDialog();
closeDialogCallback();
setDefaultState();
};
const submit = () => {
permissionAPI
.updatePermissions(scope, users)
.then((_response: any) => {
success("Users were sucessfully updated for " + label);
close();
refreshCallback();
userCallback && userCallback(users);
if (cardMode) {
load();
}
})
.catch((err: any) => {
err.response.data.error
? warning(err.response.data.error)
: error("Error occured when updating users for " + label);
close();
});
};
const changeUserPermissions = (user: pond.ITeam) => (event: any) => {
let updatedUsers = cloneDeep(users);
let permissionMapping = new Map<string, pond.Permission>([
["2", pond.Permission.PERMISSION_READ],
["3", pond.Permission.PERMISSION_WRITE],
["4", pond.Permission.PERMISSION_SHARE],
["1", pond.Permission.PERMISSION_USERS],
["6", pond.Permission.PERMISSION_FILE_MANAGEMENT]
]);
let permission = or(
permissionMapping.get(event.target.value),
pond.Permission.PERMISSION_INVALID
);
for (let i = 0; i < updatedUsers.length; i++) {
let currUser = updatedUsers[i];
if (
user &&
user.settings &&
currUser &&
currUser.settings &&
currUser.settings.key === user.settings.key
) {
let permissions = user.permissions ? cloneDeep(user.permissions) : [];
if (permissions.includes(permission)) {
permissions = permissions.filter(
(curPermission: pond.Permission) => curPermission !== permission
);
} else {
permissions.push(permission);
}
updatedUsers[i].permissions = permissions;
break;
}
}
setUsers(updatedUsers);
};
const removeUser = (user: string) => {
let updatedUsers = cloneDeep(users);
let updatedRemovedUsers = cloneDeep(removedUsers);
for (let i = 0; i < updatedUsers.length; i++) {
let currUser = updatedUsers[i];
if (currUser && currUser.settings && currUser.settings.key === user) {
updatedRemovedUsers.push(currUser.settings.key);
updatedUsers[i].permissions = [];
break;
}
}
setUsers(updatedUsers);
setRemovedUsers(updatedRemovedUsers);
};
const checkIsSelf = (checkUser: pond.ITeam): boolean => {
const id = checkUser && checkUser.settings ? checkUser.settings.key : "";
return user.id() === id;
};
const userIsRemoved = (user: pond.ITeam): boolean => {
const id = user && user.settings ? user.settings.key : "";
return removedUsers.includes(or(id, ""));
};
const objectUsersUnchanged = (): boolean => {
return isEqual(initialUsers, users);
};
const openRemoveSelfDialog = () => {
setRemoveSelfDialogIsOpen(true);
};
const openShareObjectDialog = () => {
setIsShareObjectDialogOpen(true);
};
const closeShareObjectDialog = (shared: boolean | undefined) => {
setIsShareObjectDialogOpen(false);
if (shared) {
load();
}
};
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const title = () => {
return (
<Grid container direction="column" justifyContent="space-between">
<Grid container direction="row" justifyContent="space-between">
<Grid size={{ xs: 8 }}>
Teams
<Typography variant="body2" color="textSecondary">
{label}
</Typography>
</Grid>
<Grid size={{ xs: 4 }} container justifyContent="flex-end">
{canShare && (
<Tooltip title={"Share " + label}>
<IconButton
aria-label="Share"
className={classes.iconButton}
onClick={openShareObjectDialog}>
<Share className={classes.shareIcon} />
</IconButton>
</Tooltip>
)}
</Grid>
</Grid>
{dialog && (
<Typography variant="body2" color="textSecondary" style={{ marginTop: "4px" }}>
{dialog}
</Typography>
)}
</Grid>
);
};
const userPermissionManagement = (user: Team) => {
let permissions = user.permissions;
if (
!canManageUsers ||
userIsRemoved(user) ||
//check they have all permissions
(permissions.includes(pond.Permission.PERMISSION_USERS) &&
permissions.includes(pond.Permission.PERMISSION_READ) &&
permissions.includes(pond.Permission.PERMISSION_WRITE) &&
permissions.includes(pond.Permission.PERMISSION_SHARE) &&
permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT))
) {
return null;
}
const canRead = permissions.includes(pond.Permission.PERMISSION_READ);
return (
<FormControl component="fieldset" fullWidth className={classes.userPermissions}>
<FormLabel component="legend"></FormLabel>
<FormGroup>
<Grid container direction="row" justifyContent="flex-start">
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
<FormControlLabel
control={
<Checkbox
checked={permissions.includes(pond.Permission.PERMISSION_READ)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_READ as pond.Permission}
/>
}
label="View"
labelPlacement="end"
/>
</Grid>
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_WRITE)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
/>
}
label="Edit"
labelPlacement="end"
/>
</Grid>
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_SHARE)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
/>
}
label="Share"
labelPlacement="end"
/>
</Grid>
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_USERS)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_USERS as pond.Permission}
/>
}
label="Manage Users"
labelPlacement="end"
/>
</Grid>
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
/>
}
label="File Management"
labelPlacement="end"
/>
</Grid>
</Grid>
</FormGroup>
</FormControl>
);
};
const goToTeam = (teamKey: string) => {
navigate("/teams/" + teamKey);
};
const userListItem = (user: Team) => {
let isSelf = checkIsSelf(user);
let isRemoved = userIsRemoved(user);
let name = user.name();
let permissions = user.permissions;
let userSettings = pond.TeamSettings.create(user.settings !== null ? user.settings : undefined);
return (
<ListItem
alignItems="flex-start"
sx={{
opacity: isRemoved ? 0.5 : 1,
pointerEvents: isRemoved ? 'none' : 'auto',
cursor: isRemoved ? 'not-allowed' : 'pointer',
}}
>
<ListItemAvatar>
<Avatar
alt={name}
src={
userSettings.avatar && userSettings.avatar !== "" ? userSettings.avatar : undefined
}
className={classes.userAvatar}>
{!(userSettings.avatar && userSettings.avatar !== "") && name}
</Avatar>
</ListItemAvatar>
<ListItemText
className={classes.textOverflowEllipsis}
disableTypography
primary={
<Link
onClick={() => {
goToTeam(user.key());
}}>
<Typography
component="div"
variant="body1"
color="textPrimary"
style={{ cursor: "pointer" }}>
{name + (isSelf ? " (you)" : "")}
</Typography>
</Link>
}
secondary={
<React.Fragment>
<Typography component="div" variant="caption" color="textSecondary">
{isRemoved ? "REMOVED " : userRoleFromPermissions(permissions)}
</Typography>
</React.Fragment>
}
/>
{isSelf ? (
<ListItemSecondaryAction>
<Tooltip title={"Remove yourself from " + label}>
<IconButton
edge="end"
aria-label={"Remove yourself from " + label}
onClick={openRemoveSelfDialog}>
<RemoveUserIcon className={classes.removeIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
) : (
canManageUsers &&
!isRemoved &&
(!permissions.includes(pond.Permission.PERMISSION_USERS) || canProvision) && (
<ListItemSecondaryAction>
<Tooltip title="Revoke user's access">
<IconButton
edge="end"
aria-label="Remove user from device"
onClick={() => removeUser(or(user.settings, { key: "" }).key)}>
<RemoveUserIcon className={classes.removeIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
)
)}
</ListItem>
);
};
const objectUsersList = () => {
//const sortedUsers = sortUsersByRole(users);
let userListItems: any = [];
for (var i = 0; i < users.length; i++) {
let user = users[i];
if (user) {
userListItems.push(
<React.Fragment key={i}>
{userListItem(user)}
{userPermissionManagement(user)}
<Divider />
</React.Fragment>
);
}
}
return <List>{userListItems}</List>;
};
const content = () => {
if (isLoading) {
return (
<Grid container justifyContent="center" alignContent="center" className={classes.verticalSpacing}>
<CircularProgress />
</Grid>
);
}
if (users.length < 1) {
return (
<React.Fragment>
<Typography
variant="h6"
align="center"
color="textSecondary"
className={!cardMode ? classes.verticalSpacing : ""}
style={{ padding: cardMode ? 0 : "" }}>
No teams found.
</Typography>
</React.Fragment>
);
}
return objectUsersList();
};
const actions = () => {
return (
<Grid container direction="row" justifyContent="space-between">
<Grid size={{ xs: 4 }}></Grid>
<Grid size={{ xs: 8 }} container justifyContent="flex-end">
{!cardMode && (
<Button onClick={close} color="primary">
Cancel
</Button>
)}
{canManageUsers && (
<Button onClick={submit} color="primary" disabled={objectUsersUnchanged()}>
{cardMode ? "Update" : "Submit"}
</Button>
)}
</Grid>
</Grid>
);
};
const dialogs = () => {
return (
<React.Fragment>
<RemoveSelfFromObject
scope={scope}
label={label}
isDialogOpen={removeSelfDialogIsOpen}
closeDialogCallback={closeRemoveSelfDialog}
/>
<ShareWithTeam
scope={scope}
label={label}
permissions={permissions}
isDialogOpen={isShareObjectDialogOpen}
closeDialogCallback={closeShareObjectDialog}
/>
</React.Fragment>
);
};
if (cardMode) {
return (
<React.Fragment>
<Typography variant="h6">{title()}</Typography>
<div style={{ overflowY: "scroll" }}>
<Divider />
{content()}
{actions()}
{dialogs()}
<div style={{ marginTop: theme.spacing(2) }} />
</div>
</React.Fragment>
);
}
return (
<ResponsiveDialog
fullWidth
maxWidth="xs"
open={props.isDialogOpen}
onClose={close}
aria-labelledby="object-users-dialog">
<DialogTitle id="object-users-title">{title()}</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
<DialogActions>{actions()}</DialogActions>
{dialogs()}
</ResponsiveDialog>
);
}

540
src/teams/ShareWithTeam.tsx Normal file
View file

@ -0,0 +1,540 @@
import {
Button,
Checkbox,
CircularProgress,
DialogActions,
DialogContent,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
IconButton,
Link,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
Tab,
Tabs,
TextField,
Theme,
Tooltip,
Typography
} from "@mui/material";
import {
Add as AddIcon,
Link as LinkIcon,
LinkOff,
RemoveCircle as RemoveCircleIcon
} from "@mui/icons-material";
// import { MobileDateTimePicker } from "@material-ui/pickers";
import ResponsiveDialog from "common/ResponsiveDialog";
import { usePermissionAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { binScope, Scope, ShareableLink } from "models";
import moment, { Moment } from "moment";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { openSnackbar } from "providers/Snackbar";
// import { Status } from "@sentry/react";
import { useGateAPI, useBinAPI } from "providers";
import TeamSearch from "./TeamSearch";
import { green, red } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { MobileDateTimePicker } from "@mui/x-date-pickers";
const useStyles = makeStyles((theme: Theme) => {
return ({
dialogContent: {
marginTop: theme.spacing(2)
},
formContainer: {
display: "flex",
flexWrap: "wrap"
},
formControl: {
margin: theme.spacing(1),
minWidth: 160,
width: "100%"
},
emailForm: {
paddingBottom: theme.spacing(2)
},
fabContainer: {
position: "relative",
minHeight: "64px"
},
addIcon: {
color: green[500],
"&:hover": {
color: green[600]
}
},
removeIcon: {
color: red[500],
"&:hover": {
color: red[600]
}
},
grey: {
color: theme.palette.text.secondary
}
});
});
interface Props {
scope: Scope;
label: string;
permissions: pond.Permission[];
isDialogOpen: boolean;
closeDialogCallback: Function;
}
export default function ShareObject(props: Props) {
const binAPI = useBinAPI();
const gateAPI = useGateAPI();
const classes = useStyles();
const permissionAPI = usePermissionAPI();
const { info, success } = useSnackbar();
const { scope, label, permissions, isDialogOpen, closeDialogCallback } = props;
const [sharedPermissions, setSharedPermissions] = useState<pond.Permission[]>([
pond.Permission.PERMISSION_READ
]);
const [tab, setTab] = useState<number>(0);
const prevTab = usePrevious(tab);
const [isLoadingLinks, setIsLoadingLinks] = useState<boolean>(false);
const [shareableLinks, setShareableLinks] = useState<ShareableLink[]>([]);
const [newLinkExpiration, setNewLinkExpiration] = useState<Moment>(moment().add(1, "week"));
const [isNewLinkInfinite, setIsNewLinkInfinite] = useState<boolean>(false);
const [teamKey, setTeamKey] = useState<string>("");
const share = () => {
permissionAPI.shareObjectByKey(scope, teamKey, sharedPermissions).then(resp => {
let shareBins = true;
if (resp && resp.data && resp.data.existing) {
success(label + " was shared with team");
} else {
success("Team was sent an email to sign up");
shareBins = false;
}
let successBins = true;
if (scope.kind === "binyard" && shareBins) {
binAPI.listBins(1000, 0, "asc", "name", scope.key).then(resp => {
resp.data.bins.forEach(bin => {
if (bin.settings) {
let newScope = binScope(bin.settings?.key);
permissionAPI.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(err => {
successBins = false;
});
}
});
});
if (!successBins) {
openSnackbar("error", "One or more bins failed to share");
} else {
openSnackbar("success", "Bins shared");
}
}
let successGates = true;
if (scope.kind === "terminal" && shareBins) {
gateAPI
.listGates(1000, 0, undefined, undefined, undefined, undefined, [scope.key], [scope.kind])
.then(resp => {
resp.data.gates.forEach(gate => {
if (gate) {
let newScope = { key: gate.key, kind: "gate" } as Scope;
permissionAPI.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(err => {
successGates = false;
});
}
});
});
if (!successGates) {
openSnackbar("error", "One or more Gates failed to share");
} else {
openSnackbar("success", "Gates shared");
}
}
close(true);
});
/*permissionAPI
.shareObject(scope, email, sharedPermissions)
.then((result: any) => {
let shareBins = true;
if (result && result.data && result.data.existing) {
success(label + " was shared with " + email);
} else {
success(email + " was sent an email to sign up");
shareBins = false;
}
let successBins = true;
if (scope.kind === "binyard" && shareBins) {
binAPI.listBins(1000, 0, "asc", "name", scope.key).then(resp => {
resp.data.bins.forEach(bin => {
if (bin.settings) {
let newScope = binScope(bin.settings?.key);
permissionAPI.shareObject(newScope, email, sharedPermissions).catch(err => {
successBins = false;
});
}
});
});
if (!successBins) {
openSnackbar("error", "One or more bins failed to share with " + email);
} else {
openSnackbar(Status.Success, "Bins shared with " + email);
}
}
closeAfterShare();
})
.catch((err: any) => {
error("Unable to share " + label + " with " + email);
close();
});*/
};
const loadShareableLinks = useCallback(() => {
setIsLoadingLinks(true);
permissionAPI
.listShareableLinks(scope)
.then((response: any) => {
let rawShareableLinks = response.data.links ? response.data.links : [];
let rShareableLinks = rawShareableLinks.map((rawShareableLink: any) =>
ShareableLink.any(rawShareableLink)
);
setShareableLinks(rShareableLinks);
})
.catch((error: any) => {
setShareableLinks([]);
console.log("Error occured while loading shareable links");
})
.finally(() => {
setIsLoadingLinks(false);
});
}, [permissionAPI, scope]);
useEffect(() => {
if (tab === 1 && prevTab !== tab) {
loadShareableLinks();
}
}, [loadShareableLinks, prevTab, tab]);
const getNeverExpires = () => {
return "";
};
const createShareableLink = () => {
const expiration = isNewLinkInfinite ? getNeverExpires() : newLinkExpiration.toISOString();
permissionAPI
.addShareableLink(scope, expiration)
.then((response: any) => {
loadShareableLinks();
})
.catch((error: any) => {
console.log("Error occured while creating shareable link");
});
};
const revokeShareableLink = (code: string) => {
permissionAPI
.removeShareableLink(scope, code)
.then((response: any) => {
loadShareableLinks();
})
.catch((error: any) => {
console.log("Error occured while removing the shareable link");
});
};
const getShareableLinkURL = (code: string): string => {
const base = window.location.origin;
return base + "/" + scope.kind + "s/" + code;
};
const copyShareableLinkToClipboard = (code: string) => {
const url = getShareableLinkURL(code);
navigator.clipboard.writeText(url);
info("Copied link to clipboard");
};
const close = (callback: boolean) => {
closeDialogCallback(callback);
};
const changePermissions = (event: any) => {
let updatedSharedPermissions: Array<pond.Permission> = cloneDeep(sharedPermissions);
let permission: pond.Permission;
switch (event.target.value) {
case "1":
permission = pond.Permission.PERMISSION_USERS;
break;
case "2":
permission = pond.Permission.PERMISSION_READ;
break;
case "3":
permission = pond.Permission.PERMISSION_WRITE;
break;
case "4":
permission = pond.Permission.PERMISSION_SHARE;
break;
case "6":
permission = pond.Permission.PERMISSION_FILE_MANAGEMENT;
break;
default:
permission = pond.Permission.PERMISSION_INVALID;
break;
}
if (updatedSharedPermissions.includes(permission)) {
updatedSharedPermissions = updatedSharedPermissions.filter(
(curPermission: pond.Permission) => curPermission !== permission
);
} else {
updatedSharedPermissions.push(permission);
}
setSharedPermissions(updatedSharedPermissions);
};
const changeTab = (event: any, newTab: number) => {
setTab(newTab);
};
const isValid = () => {
return teamKey !== "" && sharedPermissions.length > 0;
};
// UI BEGINS
const teamsList = () => {
return <TeamSearch label="Team" setTeamCallback={setTeamKey} />;
};
const permissionsForm = () => {
return (
<FormControl component="fieldset" fullWidth variant="outlined">
<FormLabel component="legend">Permissions</FormLabel>
<FormGroup>
{permissions.includes(pond.Permission.PERMISSION_READ) && (
<FormControlLabel
control={
<Checkbox
disabled={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
checked={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_READ as pond.Permission}
/>
}
label="View"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_WRITE)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
/>
}
label="Edit"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_SHARE)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
/>
}
label="Share"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_USERS)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_USERS as pond.Permission}
/>
}
label="Manage Users"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
/>
}
label="Files"
labelPlacement="end"
/>
)}
</FormGroup>
</FormControl>
);
};
const emailSharing = () => {
return (
<React.Fragment>
{teamsList()}
<div style={{ width: "auto", height: "1rem" }} />
{permissionsForm()}
</React.Fragment>
);
};
const shareableLinksList = () => {
const now = moment();
let items: any[] = [];
shareableLinks.forEach((link: ShareableLink) => {
const expiration = moment(link.settings.expiration);
const neverExpires = link.settings.expiration === getNeverExpires();
const expired = !neverExpires && expiration < now;
items.push(
<ListItem key={link.key()}>
{expired ? (
<IconButton disabled>
<LinkOff />
</IconButton>
) : (
<Tooltip title="Click to copy link">
<IconButton onClick={() => copyShareableLinkToClipboard(link.key())}>
<LinkIcon />
</IconButton>
</Tooltip>
)}
<ListItemText
primary={
expired ? (
<span className={classes.grey}>{"/devices/" + link.key()}</span>
) : (
<Link href={getShareableLinkURL(link.key())} target="_blank">
{"/devices/" + link.key()}
</Link>
)
}
secondary={
neverExpires ? (
"Never expires"
) : (
<Tooltip title={expiration.calendar()}>
<span>{(expired ? "Expired " : "Expires ") + expiration.fromNow()}</span>
</Tooltip>
)
}
/>
<ListItemSecondaryAction>
<Tooltip title="Revoke shareable link">
<IconButton onClick={() => revokeShareableLink(link.key())}>
<RemoveCircleIcon className={classes.removeIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
});
return (
<List>
{isLoadingLinks ? <CircularProgress /> : <React.Fragment>{items}</React.Fragment>}
{shareableLinks.length > 0 && <Divider variant="middle" />}
<ListItem>
<FormGroup row>
<MobileDateTimePicker
disabled={isNewLinkInfinite}
// renderInput={props => <TextField {...props} helperText="" />}
label="Expiration Date"
value={newLinkExpiration}
onChange={date => setNewLinkExpiration(date as Moment)}
disablePast
/>
<FormControlLabel
control={
<Checkbox
checked={isNewLinkInfinite}
onChange={(_, checked) => setIsNewLinkInfinite(checked)}
value="isNewLinkInfinite"
color="secondary"
/>
}
label="Never expires"
/>
</FormGroup>
<ListItemSecondaryAction>
<Tooltip title="Create new shareable link">
<IconButton
className={classes.addIcon}
color="default"
onClick={() => createShareableLink()}>
<AddIcon className={classes.addIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
</List>
);
};
const content = () => {
switch (tab) {
case 1:
return <React.Fragment>{shareableLinksList()}</React.Fragment>;
default:
return <React.Fragment>{emailSharing()}</React.Fragment>;
}
};
return (
<ResponsiveDialog
maxWidth="sm"
fullWidth
open={isDialogOpen}
onClose={() => close(false)}
aria-labelledby="share-dialog-title">
<DialogTitle id="share-dialog-title">
Share
<Typography variant="body2" color="textSecondary">
{label}
</Typography>
</DialogTitle>
{scope.kind === "device" && (
<Tabs value={tab} onChange={changeTab}>
<Tab label="Share by Email" />
<Tab label="Shareable Links" />
</Tabs>
)}
<Divider />
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
<DialogActions>
<Button onClick={() => close(false)} color="primary">
Cancel
</Button>
{tab === 0 && (
<Button onClick={share} color="primary" disabled={!isValid()}>
Share
</Button>
)}
</DialogActions>
</ResponsiveDialog>
);
}

98
src/teams/TeamSearch.tsx Normal file
View file

@ -0,0 +1,98 @@
import { Box } from "@mui/material";
import SearchSelect, { Option } from "common/SearchSelect";
import { useGlobalState, useTeamAPI, useUserAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
interface Props {
className?: string;
setTeamCallback?: React.Dispatch<React.SetStateAction<string>>;
style?: React.CSSProperties;
label?: string;
loadUsers?: boolean;
}
export default function TeamSearch(props: Props) {
const { className, setTeamCallback, style, label, loadUsers } = props;
const teamAPI = useTeamAPI();
const userAPI = useUserAPI();
const [selectedTeam, setSelectedTeam] = useState<Option | null>(null);
const [teams, setTeams] = useState<Option[]>([]);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [{ user }] = useGlobalState();
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setSearch(e.target.value);
};
const loadTeams = useCallback(() => {
let options: Option[] = [];
setLoading(true);
teamAPI
.listTeams(10, 0, "desc", undefined, search)
.then(resp => {
if (loadUsers)
options.push({
value: user.id(),
label: "Yourself",
icon: "default",
group: "Users"
});
resp.data.teams.forEach(team => {
let o = {
value: team.settings?.key,
label: team.settings!.name,
icon: team.settings?.avatar,
group: "Teams"
};
options.push(o);
});
})
.finally(() => {
if (loadUsers) {
userAPI
.listUsers(10, 0, "desc", undefined, search)
.then(resp => {
resp.users.forEach(user => {
options.push({
value: user.id(),
label: user.name(),
icon: user.settings.avatar,
group: "Users"
});
});
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
setTeams(options);
});
}, [teamAPI, loadUsers, userAPI, user, search]);
useEffect(() => {
loadTeams();
}, [loadTeams]);
return (
<Box className={className} style={style}>
<SearchSelect
getOptionSelected={() => {
return true;
}}
label={label ? label : "View As"}
options={teams}
loading={loading}
group
selected={selectedTeam}
onChange={onChange}
changeSelection={(option: Option | null) => {
setSelectedTeam(option);
if (setTeamCallback) setTeamCallback(option?.value);
}}
/>
</Box>
);
}