Merge pull request #427 from bluewave-labs/feat/team-panel

Team panel table
This commit is contained in:
Alexander Holliday
2024-07-23 10:21:37 -07:00
committed by GitHub
5 changed files with 207 additions and 395 deletions
@@ -2,24 +2,19 @@ import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import {
Box,
Checkbox,
Container,
ButtonGroup,
Divider,
IconButton,
MenuItem,
Modal,
Select,
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Typography,
} from "@mui/material";
import ButtonSpinner from "../../ButtonSpinner";
import Button from "../../Button";
import { useState } from "react";
import { useEffect, useState } from "react";
import EditSvg from "../../../assets/icons/edit.svg?react";
import Field from "../../Inputs/Field";
import { credentials } from "../../../Validation/validation";
@@ -27,6 +22,8 @@ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import axiosInstance from "../../../Utils/axiosConfig";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import BasicTable from "../../BasicTable";
import Remove from "../../../assets/icons/trash-bin.svg?react";
/**
* TeamPanel component manages the organization and team members,
@@ -36,138 +33,114 @@ import { useSelector } from "react-redux";
* @returns {JSX.Element}
*/
const teamColumns = [
{
id: "checkbox",
label: "",
sx: { minWidth: "20px", width: "40px" },
},
{
id: "name",
label: "NAME",
sx: { fontSize: "12px" },
},
{ id: "email", label: "EMAIL", sx: { fontSize: "12px" } },
{ id: "role", label: "ROLE", sx: { fontSize: "12px" } },
];
//for testing, will be removed later
const teamConfig = [
{
id: 0,
isChecked: false,
name: "John Connor",
email: "john@domain.com",
type: "admin",
role: "Administrator",
createdAt: "10/4/2022",
},
{
id: 1,
isChecked: false,
name: "Adam McFadden",
email: "adam@domain.com",
type: "member",
role: "Member",
createdAt: "10/4/2022",
},
{
id: 2,
isChecked: false,
name: "Cris Cross",
email: "cris@domain.com",
type: "member",
role: "Member",
createdAt: "10/4/2022",
},
{
id: 3,
isChecked: false,
name: "Prince",
email: "prince@domain.com",
type: "member",
role: "Member",
createdAt: "10/4/2022",
},
];
const actionsConfig = [
{
value: "bulk",
label: "Bulk actions",
},
];
const roleConfig = [
{
value: "role",
label: "Change role to",
},
];
const TeamPanel = () => {
const theme = useTheme();
//TODO - use redux loading state
//!! - currently all loading buttons are tied to the same state
const [isLoading, setIsLoading] = useState(false);
//TODO - connect to redux
const { authToken } = useSelector((state) => state.auth);
//TODO
const [orgStates, setOrgStates] = useState({
name: "Bluewave Labs",
isLoading: false,
isEdit: false,
});
const [toInvite, setToInvite] = useState({
email: "",
role: "",
});
const [tableData, setTableData] = useState({});
const [members, setMembers] = useState([]);
const [filter, setFilter] = useState("all");
const [errors, setErrors] = useState({});
useEffect(() => {
const fetchTeam = async () => {
try {
const response = await axiosInstance.get("/auth/users", {
headers: { Authorization: `Bearer ${authToken}` },
});
setMembers(response.data.data);
} catch (error) {
createToast({
body: error.message || "Error fetching team members.",
});
}
};
fetchTeam();
}, []);
useEffect(() => {
let team = members;
if (filter !== "all")
team = members.filter((member) => member.role[0] === filter);
const data = {
cols: [
{ id: 1, name: "NAME" },
{ id: 2, name: "EMAIL" },
{ id: 3, name: "ROLE" },
{ id: 4, name: "ACTION" },
],
rows: team?.map((member, idx) => {
return {
id: member._id,
data: [
{
id: idx,
data: (
<Stack>
<Typography
style={{ color: theme.palette.otherColors.blackish }}
>
{member.firstName + " " + member.lastName}
</Typography>
<Typography sx={{ opacity: 0.6 }}>
Created {new Date(member.createdAt).toLocaleDateString()}
</Typography>
</Stack>
),
},
{ id: idx + 1, data: member.email },
{
// TODO - Add select dropdown
id: idx + 2,
data: member.role[0] === "admin" ? "Administrator" : "Member",
},
{
// TODO - Add delete onClick
id: idx + 3,
data: (
<IconButton
aria-label="remove member"
sx={{
"&:focus": {
outline: "none",
},
}}
>
<Remove />
</IconButton>
),
},
],
};
}),
};
setTableData(data);
}, [members, filter]);
// RENAME ORGANIZATION
const toggleEdit = () => {
setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
};
const handleRename = () => {};
const [teamStates, setTeamStates] = useState({
members: teamConfig,
filter: "",
});
const handleCheckCell = (id) => {
const updatedTeamStates = [...teamStates.members];
updatedTeamStates[id] = {
...updatedTeamStates[id],
isChecked: !updatedTeamStates[id].isChecked,
};
setTeamStates((prev) => ({
...prev,
members: updatedTeamStates,
}));
};
const handleFilter = (filter) => {
setTeamStates((prev) => ({
...prev,
filter: filter,
}));
};
//TODO - implement select action function
const handleSelectActionType = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 2000);
};
//TODO - implement select role function
const handleSelectRoleType = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 2000);
};
//TODO - implement save team function
const handleSaveTeam = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 2000);
};
const handleSaveTeam = () => {};
const { authToken } = useSelector((state) => state.auth);
const [toInvite, setToInvite] = useState({
email: "",
role: "",
});
const [errors, setErrors] = useState({});
// INVITE MEMBER
const [isOpen, setIsOpen] = useState(false);
const handleChange = (event) => {
const { value } = event.target;
@@ -192,7 +165,7 @@ const TeamPanel = () => {
return updatedErrors;
});
};
const [isOpen, setIsOpen] = useState(false);
const handleInviteMember = async () => {
if (toInvite.role !== "user" || toInvite !== "admin")
setToInvite((prev) => ({ ...prev, role: "user" }));
@@ -233,97 +206,97 @@ const TeamPanel = () => {
setErrors({});
};
const handleMembersQuery = (type) => {
let count = 0;
teamStates.members.forEach((member) => {
type === "" ? count++ : member.type === type ? count++ : "";
});
return count;
};
return (
<TabPanel value="team">
<form className="edit-organization-form">
<div className="edit-organization-form__wrapper">
<Stack>
<Typography component="h1">Organization name</Typography>
</Stack>
<Stack
className="row-stack"
direction="row"
justifyContent="flex-end"
alignItems="center"
sx={{ minHeight: "34px", maxHeight: "34px" }}
>
<TextField
value={orgStates.name}
onChange={(event) =>
setOrgStates((prev) => ({
...prev,
name: event.target.value,
}))
}
disabled={!orgStates.isEdit}
sx={{
color: theme.palette.otherColors.bluishGray,
"& .Mui-disabled": {
WebkitTextFillColor: "initial !important",
},
"& .Mui-disabled fieldset": {
borderColor: "transparent !important",
},
}}
inputProps={{
sx: { textAlign: "end", padding: theme.gap.small },
}}
/>
<Button
level={orgStates.isEdit ? "secondary" : "tertiary"}
label={orgStates.isEdit ? "Save" : ""}
img={!orgStates.isEdit ? <EditSvg /> : ""}
onClick={() => toggleEdit()}
sx={{
minWidth: 0,
paddingX: theme.gap.small,
ml: orgStates.isEdit ? theme.gap.small : 0,
}}
/>
</Stack>
</div>
<Divider
aria-hidden="true"
className="short-divider"
sx={{ marginY: theme.spacing(4) }}
/>
<Box sx={{ alignSelf: "flex-start" }}>
<Typography component="h1">Organization name</Typography>
</Box>
<Stack
className="row-stack"
direction="row"
justifyContent="flex-end"
alignItems="center"
sx={{ height: "34px" }}
>
<TextField
value={orgStates.name}
onChange={(event) =>
setOrgStates((prev) => ({
...prev,
name: event.target.value,
}))
}
disabled={!orgStates.isEdit}
sx={{
color: theme.palette.otherColors.bluishGray,
"& .Mui-disabled": {
WebkitTextFillColor: "initial !important",
},
"& .Mui-disabled fieldset": {
borderColor: "transparent !important",
},
}}
inputProps={{
sx: { textAlign: "end", padding: theme.gap.small },
}}
/>
<Button
level={orgStates.isEdit ? "secondary" : "tertiary"}
label={orgStates.isEdit ? "Save" : ""}
img={!orgStates.isEdit ? <EditSvg /> : ""}
onClick={() => toggleEdit()}
sx={{
minWidth: 0,
paddingX: theme.gap.small,
ml: orgStates.isEdit ? theme.gap.small : 0,
}}
/>
</Stack>
</form>
<form className="edit-team-form" noValidate spellCheck="false">
<div className="edit-team-form__wrapper">
<Typography component="h1">Team members</Typography>
</div>
<div className="edit-team-form__wrapper compact">
<Divider
aria-hidden="true"
className="short-divider"
sx={{ marginY: theme.spacing(4) }}
/>
<form
className="edit-team-form"
noValidate
spellCheck="false"
style={{
display: "flex",
flexDirection: "column",
gap: theme.gap.large,
}}
>
<Typography component="h1">Team members</Typography>
<Stack direction="row" justifyContent="space-between">
<Stack
direction="row"
gap="20px"
alignItems="center"
alignItems="flex-end"
gap={theme.gap.medium}
sx={{ fontSize: "14px" }}
>
<Box onClick={() => handleFilter("")}>
All
<span className="members-query">
<span>{handleMembersQuery("")}</span>
</span>
</Box>
<Box onClick={() => handleFilter("admin")}>
Administrator
<span className="members-query">
<span>{handleMembersQuery("admin")}</span>
</span>
</Box>
<Box onClick={() => handleFilter("member")}>
Member
<span className="members-query">
<span>{handleMembersQuery("member")}</span>
</span>
</Box>
<ButtonGroup>
<Button
level="secondary"
label="All"
onClick={() => setFilter("all")}
sx={{ backgroundColor: filter === "all" && "#f4f4f4" }}
/>
<Button
level="secondary"
label="Administrator"
onClick={() => setFilter("admin")}
sx={{ backgroundColor: filter === "admin" && "#f4f4f4" }}
/>
<Button
level="secondary"
label="Member"
onClick={() => setFilter("user")}
sx={{ backgroundColor: filter === "user" && "#f4f4f4" }}
/>
</ButtonGroup>
</Stack>
<Button
level="primary"
@@ -331,146 +304,17 @@ const TeamPanel = () => {
sx={{ paddingX: "30px" }}
onClick={() => setIsOpen(true)}
/>
</div>
<div className="edit-team-form__wrapper compact">
<Container
disableGutters
sx={{
border: `1px solid ${theme.palette.section.borderColor}`,
borderRadius: `${theme.shape.borderRadius}px`,
borderBottom: "none",
}}
>
<Stack direction="row" gap="40px" p="20px">
<Stack
direction="row"
gap="10px"
alignItems="center"
className="table-stack"
>
<Select
id="select-actions"
value="bulk"
inputProps={{ id: "select-actions-input" }}
>
{actionsConfig.map((action) => (
<MenuItem
value={action.value}
key={action.value}
sx={{ fontSize: "13px" }}
>
{action.label}
</MenuItem>
))}
</Select>
<ButtonSpinner
level="secondary"
label="Apply"
onClick={handleSelectActionType}
isLoading={isLoading}
sx={{
bgcolor: "#fafafa",
}}
/>
</Stack>
<Stack direction="row" gap="10px" alignItems="center">
<Select
id="select-role"
value="role"
inputProps={{ id: "select-role-input" }}
>
{roleConfig.map((role) => (
<MenuItem
value={role.value}
key={role.value}
sx={{ fontSize: "13px" }}
>
{role.label}
</MenuItem>
))}
</Select>
<ButtonSpinner
level="secondary"
label="Apply"
onClick={handleSelectRoleType}
isLoading={isLoading}
sx={{
bgcolor: "#fafafa",
}}
/>
</Stack>
</Stack>
<Table
sx={{
borderTop: `1px solid ${theme.palette.section.borderColor}`,
tableLayout: "fixed",
}}
>
<TableHead>
<TableRow
sx={{
bgcolor: "#fafafa",
}}
>
{teamColumns.map((cell) => (
<TableCell
key={cell.id}
sx={{
...cell.sx,
color: theme.palette.otherColors.slateGray,
}}
>
{cell.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{teamStates.members.map((cell) =>
teamStates.filter === "" ||
teamStates.filter === cell.type ? (
<TableRow key={cell.id}>
<TableCell align="center">
<Checkbox
id={`${cell.id}-${cell.name}`}
checked={cell.isChecked}
onChange={() => handleCheckCell(cell.id)}
inputProps={{ "aria-label": "controlled" }}
/>
</TableCell>
<TableCell>
<Stack direction="column">
<Box
sx={{
color: theme.palette.otherColors.blackish,
verticalAlign: "top",
}}
>
{cell.name}
</Box>
<Box>Created at {cell.createdAt}</Box>
</Stack>
</TableCell>
<TableCell>{cell.email}</TableCell>
<TableCell>{cell.role}</TableCell>
</TableRow>
) : (
""
)
)}
</TableBody>
</Table>
</Container>
</div>
<Divider aria-hidden="true" width="0" />
</Stack>
<BasicTable data={tableData} paginated={false} reversed={true} />
<Stack direction="row" justifyContent="flex-end">
<Box width="fit-content">
<ButtonSpinner
level="primary"
label="Save"
onClick={handleSaveTeam}
isLoading={isLoading}
isLoading={false}
loadingText="Saving..."
disabled={true}
sx={{
paddingX: "40px",
}}
@@ -586,7 +430,7 @@ const TeamPanel = () => {
level="primary"
label="Send invite"
onClick={handleInviteMember}
isLoading={isLoading}
isLoading={false}
disabled={Object.keys(errors).length !== 0}
/>
</Stack>
+14 -49
View File
@@ -28,8 +28,8 @@
margin: 50px 0;
}
.account .MuiDivider-root,
.account .MuiOutlinedInput-root fieldset {
border-color: var(--env-var-color-29);
.account .MuiButtonGroup-root button {
border-color: var(--env-var-color-16);
}
.account [class$="-form"] {
width: inherit;
@@ -42,8 +42,7 @@
align-items: flex-start;
}
.edit-profile-form__wrapper > .MuiStack-root:not(.row-stack),
.edit-password-form__wrapper > .MuiStack-root:not(.row-stack),
.edit-organization-form__wrapper > .MuiStack-root:not(.row-stack) {
.edit-password-form__wrapper > .MuiStack-root:not(.row-stack) {
flex-direction: column;
gap: 8px;
margin-right: 10px;
@@ -68,21 +67,6 @@
align-items: center;
}
.edit-team-form__wrapper td {
padding: 10px 0;
padding-top: 20px;
vertical-align: text-top;
}
.edit-team-form__wrapper th {
padding: var(--env-var-spacing-1) 0;
}
.edit-team-form__wrapper th:first-of-type {
padding-left: 20px;
}
.edit-team-form__wrapper td {
color: var(--env-var-color-25);
}
#select-actions,
#select-role {
padding: 5px 0;
@@ -101,24 +85,6 @@
border-color: var(--env-var-color-2);
}
.MuiStack-root:has(.MuiBox-root > .members-query) .MuiBox-root {
cursor: pointer;
}
.members-query {
display: inline-flex;
justify-content: center;
align-items: center;
margin-left: 6px;
padding: 2px;
font-size: 12px;
font-weight: 500;
color: var(--env-var-color-3);
background-color: #e0e9fd;
min-width: 22px;
min-height: 22px;
border-radius: 50%;
}
/* for testing, will remove later */
@media only screen and (max-width: 1600px) {
.edit-profile-form__wrapper .MuiStack-root:not(.row-stack),
@@ -131,22 +97,10 @@
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.account .text-field-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 18px !important;
height: 18px !important;
opacity: 0.8;
}
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
position: absolute;
top: 100%;
}
.account .email-text-field,
.account .password-text-field {
margin: 0;
}
.edit-profile-form__wrapper .MuiFormControl-root,
.edit-password-form__wrapper .MuiFormControl-root {
@@ -173,3 +127,14 @@
.MuiInputBase-root:not(.Mui-focused):has(#team-member-role):hover fieldset {
border-color: var(--env-var-color-29);
}
.edit-organization-form {
display: flex;
align-items: center;
}
.edit-team-form .MuiTableCell-root {
color: var(--env-var-color-25) !important;
}
.edit-team-form .MuiTableBody-root .MuiTableCell-root {
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2) !important;
}
+1 -1
View File
@@ -38,7 +38,7 @@ const Account = ({ open = "profile" }) => {
<Box
sx={{
borderBottom: 1,
borderColor: theme.palette.section.borderColor,
borderColor: "var(--env-var-color-16)",
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
>
+1 -1
View File
@@ -130,7 +130,7 @@ const Monitors = () => {
>
<div className="monitors-bar">
<div className="monitors-bar-title">
Hello, {authState.user.firstname}
Hello, {authState.user.firstName}
</div>
<Button
level="primary"
+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3333 5V4.33333C12.3333 3.39991 12.3333 2.9332 12.1517 2.57668C11.9919 2.26308 11.7369 2.00811 11.4233 1.84832C11.0668 1.66667 10.6001 1.66667 9.66667 1.66667H8.33333C7.39991 1.66667 6.9332 1.66667 6.57668 1.84832C6.26308 2.00811 6.00811 2.26308 5.84832 2.57668C5.66667 2.9332 5.66667 3.39991 5.66667 4.33333V5M7.33333 9.58333V13.75M10.6667 9.58333V13.75M1.5 5H16.5M14.8333 5V14.3333C14.8333 15.7335 14.8333 16.4335 14.5608 16.9683C14.3212 17.4387 13.9387 17.8212 13.4683 18.0609C12.9335 18.3333 12.2335 18.3333 10.8333 18.3333H7.16667C5.76654 18.3333 5.06647 18.3333 4.53169 18.0609C4.06129 17.8212 3.67883 17.4387 3.43915 16.9683C3.16667 16.4335 3.16667 15.7335 3.16667 14.3333V5" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 883 B