diff --git a/web/.eslintrc.js b/web/.eslintrc.js index b87ebf3..c8cc932 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -47,5 +47,33 @@ module.exports = { "react/jsx-props-no-spreading": 0, "react-hooks/exhaustive-deps": "off", "@typescript-eslint/no-non-null-assertion": 0, + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index", + "object", + "type", + ], + pathGroups: [ + { + pattern: "react", + group: "external", + position: "before", + }, + ], + pathGroupsExcludedImportTypes: ["react"], + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], }, -}; \ No newline at end of file +}; diff --git a/web/config-overrides.js b/web/config-overrides.js index 9c6f894..dba303c 100644 --- a/web/config-overrides.js +++ b/web/config-overrides.js @@ -4,7 +4,7 @@ module.exports = function override(config) { ...config.resolve, alias: { ...config.alias, -"@components": path.resolve(__dirname, "src/components"), + "@components": path.resolve(__dirname, "src/components"), "@consts": path.resolve(__dirname, "src/consts"), "@interfaces": path.resolve(__dirname, "src/interfaces"), "@janush-types": path.resolve(__dirname, "src/types"), @@ -13,6 +13,7 @@ module.exports = function override(config) { "@layouts": path.resolve(__dirname, "src/layouts"), "@routing": path.resolve(__dirname, "src/routing"), "@themes": path.resolve(__dirname, "src/themes"), + "@validations": path.resolve(__dirname, "src/validations"), }, }; return config; diff --git a/web/package.json b/web/package.json index ab79a95..0161724 100644 --- a/web/package.json +++ b/web/package.json @@ -10,13 +10,14 @@ "@mui/icons-material": "^5.0.0-rc.1", "@mui/material": "^5.0.0-rc.1", "@mui/styles": "^5.0.0-rc.1", + "@mui/x-data-grid": "^5.10.0", "aws-amplify": "^4.2.10", "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", "react-hook-form": "^7.15.4", - "react-router-dom": "^5.3.0", + "react-router-dom": "^6.3.0", "react-scripts": "4.0.3", "web-vitals": "^2.1.4", "yup": "^0.32.9" @@ -103,4 +104,4 @@ "html" ] } -} \ No newline at end of file +} diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index e7c16bf..20a1d2f 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -1,4 +1,5 @@ import { render } from "@testing-library/react"; + import App from "./App"; describe("", () => { diff --git a/web/src/App.tsx b/web/src/App.tsx index 5571fd0..855bb19 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,11 +1,11 @@ import React from "react"; import { Providers } from "@features/Providers/Providers"; -import { Routes } from "@routing/Routes"; +import { Router } from "@routing/Routes"; const App: React.VFC = () => ( - + ); diff --git a/web/src/components/AuthBottomBar/AuthBottomBar.test.tsx b/web/src/components/AuthBottomBar/AuthBottomBar.test.tsx index 8cd82df..172d235 100644 --- a/web/src/components/AuthBottomBar/AuthBottomBar.test.tsx +++ b/web/src/components/AuthBottomBar/AuthBottomBar.test.tsx @@ -1,7 +1,6 @@ -import { MemoryRouter } from "react-router-dom"; -import { render } from "@testing-library/react"; - import { AuthBottomBar } from "@components/AuthBottomBar/AuthBottomBar"; +import { render } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; describe("", () => { it("should render properly", () => { diff --git a/web/src/components/AuthBottomBar/AuthBottomBar.tsx b/web/src/components/AuthBottomBar/AuthBottomBar.tsx index 6cc180c..6c7d090 100644 --- a/web/src/components/AuthBottomBar/AuthBottomBar.tsx +++ b/web/src/components/AuthBottomBar/AuthBottomBar.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { Box, Button, Grid, Typography } from "@mui/material"; import { Link } from "@components/Link/Link"; +import { Box, Button, Grid, Typography } from "@mui/material"; interface Props { buttonText: string; diff --git a/web/src/components/Button/Button.tsx b/web/src/components/Button/Button.tsx new file mode 100644 index 0000000..fd36a4e --- /dev/null +++ b/web/src/components/Button/Button.tsx @@ -0,0 +1,38 @@ +import { FC } from "react"; + +import { Button as MuiButton, Theme, ButtonProps } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { rgbaColors } from "@themes/palette"; + +export const Button: FC = ({ children, ...buttonProps }) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; diff --git a/web/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/web/src/components/ConfirmationDialog/ConfirmationDialog.tsx new file mode 100644 index 0000000..f1a1bdf --- /dev/null +++ b/web/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -0,0 +1,85 @@ +import { FC, ReactNode } from "react"; + +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Theme, + Button, +} from "@mui/material"; +import { useTheme } from "@mui/material/styles"; + +interface Props { + children: ReactNode; + isOpen: boolean; + onSubmit: () => void; + onCancelClick: () => void; + submitButtonTitle: string; +} + +const ConfirmationDialogTitle: FC = ({ children }) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; + +const ConfirmationDialogContent: FC = ({ children }) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; + +export const ConfirmationDialog = ({ + children, + isOpen, + onSubmit, + onCancelClick, + submitButtonTitle, +}: Props) => { + const theme = useTheme(); + + return ( + + {children} + + + + + + ); +}; + +ConfirmationDialog.Title = ConfirmationDialogTitle; +ConfirmationDialog.Content = ConfirmationDialogContent; diff --git a/web/src/components/EmailField/EmailField.tsx b/web/src/components/EmailField/EmailField.tsx index a468d5f..507f300 100644 --- a/web/src/components/EmailField/EmailField.tsx +++ b/web/src/components/EmailField/EmailField.tsx @@ -1,8 +1,7 @@ +import { TextField } from "@components/TextField/TextField"; import { Mail } from "@mui/icons-material"; import { InputAdornment, StandardTextFieldProps } from "@mui/material"; -import { TextField } from "@components/TextField/TextField"; - interface Props extends StandardTextFieldProps { errorMessage?: string | undefined; } diff --git a/web/src/components/ErrorNotification/ErrorNotification.tsx b/web/src/components/ErrorNotification/ErrorNotification.tsx new file mode 100644 index 0000000..9824a26 --- /dev/null +++ b/web/src/components/ErrorNotification/ErrorNotification.tsx @@ -0,0 +1,37 @@ +import { VFC } from "react"; + +import { Theme, Snackbar, Alert } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; + +interface Props { + content?: string; + show: boolean; + onClose: () => void; +} + +export const ErrorNotification: VFC = ({ content, show, onClose }) => { + const theme = useTheme(); + + return ( + + + {content || "Something went wrong. Please try again."} + + + ); +}; diff --git a/web/src/components/FormInput/FormInput.tsx b/web/src/components/FormInput/FormInput.tsx new file mode 100644 index 0000000..2a60e3a --- /dev/null +++ b/web/src/components/FormInput/FormInput.tsx @@ -0,0 +1,20 @@ +import { VFC } from "react"; + +import { TextField, StandardTextFieldProps } from "@mui/material"; + +interface Props extends StandardTextFieldProps { + errorMessage?: string | undefined; +} + +export const FormInput: VFC = ({ + errorMessage, + ...restProps +}: Props) => ( + +); diff --git a/web/src/components/Link/Link.tsx b/web/src/components/Link/Link.tsx index db5ed47..ad1dce5 100644 --- a/web/src/components/Link/Link.tsx +++ b/web/src/components/Link/Link.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { Link as RouterLink } from "react-router-dom"; + import { Link as MuiLink, LinkProps } from "@mui/material"; +import { Link as RouterLink } from "react-router-dom"; type Props = Omit & { to: string; diff --git a/web/src/components/ListElement/ListElement.tsx b/web/src/components/ListElement/ListElement.tsx new file mode 100644 index 0000000..e4e0882 --- /dev/null +++ b/web/src/components/ListElement/ListElement.tsx @@ -0,0 +1,29 @@ +import { VFC } from "react"; + +import { Box, Typography, Theme } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; + +interface Props { + label: string; + value: string; +} + +export const ListElement: VFC = ({ label, value }) => { + const theme = useTheme(); + + return ( + + + {label} + + + {value} + + + ); +}; diff --git a/web/src/components/PasswordField/PasswordField.test.tsx b/web/src/components/PasswordField/PasswordField.test.tsx index bfcbe62..85b3f4c 100644 --- a/web/src/components/PasswordField/PasswordField.test.tsx +++ b/web/src/components/PasswordField/PasswordField.test.tsx @@ -1,8 +1,9 @@ import React from "react"; + +import { ThemeProvider } from "@features/ThemeProvider/ThemeProvider"; import { fireEvent, render } from "@testing-library/react"; import { useForm } from "react-hook-form"; -import { ThemeProvider } from "@features/ThemeProvider/ThemeProvider"; import { PasswordField } from "./PasswordField"; const setup = () => { diff --git a/web/src/components/PasswordField/PasswordField.tsx b/web/src/components/PasswordField/PasswordField.tsx index 825b55d..34b0941 100644 --- a/web/src/components/PasswordField/PasswordField.tsx +++ b/web/src/components/PasswordField/PasswordField.tsx @@ -1,4 +1,6 @@ import React, { useState } from "react"; + +import { TextField } from "@components/TextField/TextField"; import { Lock, Visibility, VisibilityOff } from "@mui/icons-material"; import { StandardTextFieldProps, @@ -8,8 +10,6 @@ import { Tooltip, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; - -import { TextField } from "@components/TextField/TextField"; import { formDataTestId } from "@utils/formDataTestId/formDataTestId"; interface Props extends StandardTextFieldProps { diff --git a/web/src/components/Select/Select.tsx b/web/src/components/Select/Select.tsx new file mode 100644 index 0000000..a03f4e7 --- /dev/null +++ b/web/src/components/Select/Select.tsx @@ -0,0 +1,29 @@ +import { FC } from "react"; + +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import { Select as MuiSelect, MenuItem, SelectProps } from "@mui/material"; + +interface Option { + name: string; + label: string; +} + +interface Props extends SelectProps { + options: Option[]; +} + +export const Select: FC = ({ options, ...selectProps }) => { + return ( + + {options.map((option) => ( + + {option.label} + + ))} + + ); +}; diff --git a/web/src/components/Table/Table.tsx b/web/src/components/Table/Table.tsx new file mode 100644 index 0000000..a167339 --- /dev/null +++ b/web/src/components/Table/Table.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from "react"; + +import { + Table as MuiTable, + TableContainer, + TableHead, + TableBody, + Paper, +} from "@mui/material"; + +import { TableCell } from "./TableCell"; +import { TableRow } from "./TableRow"; + +interface Props { + children: ReactNode; + dataTestId?: string; +} + +export const Table = ({ children, ...restProps }: Props) => ( + + {children} + +); + +Table.TableCell = TableCell; +Table.TableBody = TableBody; +Table.TableHead = TableHead; +Table.TableRow = TableRow; diff --git a/web/src/components/Table/TableCell.tsx b/web/src/components/Table/TableCell.tsx new file mode 100644 index 0000000..036a466 --- /dev/null +++ b/web/src/components/Table/TableCell.tsx @@ -0,0 +1,30 @@ +import { FC, ReactNode } from "react"; + +import { + TableCell as MuiTableCell, + TableCellProps, + Theme, +} from "@mui/material"; +import { useTheme } from "@mui/material/styles"; + +interface Props extends TableCellProps { + children: ReactNode; +} + +export const TableCell: FC = ({ children, ...tableCellProps }) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; diff --git a/web/src/components/Table/TableRow.tsx b/web/src/components/Table/TableRow.tsx new file mode 100644 index 0000000..7cd35e7 --- /dev/null +++ b/web/src/components/Table/TableRow.tsx @@ -0,0 +1,11 @@ +import { ReactNode, FC } from "react"; + +import { TableRow as MuiTableRow, TableRowProps } from "@mui/material"; + +interface Props extends TableRowProps { + children: ReactNode; +} + +export const TableRow: FC = ({ children, ...rowProps }) => ( + {children} +); diff --git a/web/src/components/icons/ArrowLeftIcon/ArrowLeftIcon.tsx b/web/src/components/icons/ArrowLeftIcon/ArrowLeftIcon.tsx new file mode 100644 index 0000000..b75fb13 --- /dev/null +++ b/web/src/components/icons/ArrowLeftIcon/ArrowLeftIcon.tsx @@ -0,0 +1,18 @@ +import { SVGProps } from "react"; + +export const ArrowLeftIcon = (props: SVGProps) => ( + + + +); diff --git a/web/src/components/icons/SearchIcon/SearchIcon.tsx b/web/src/components/icons/SearchIcon/SearchIcon.tsx new file mode 100644 index 0000000..d1bc30b --- /dev/null +++ b/web/src/components/icons/SearchIcon/SearchIcon.tsx @@ -0,0 +1,17 @@ +import { SVGProps } from "react"; + +export const SearchIcon = (props: SVGProps) => ( + + + +); diff --git a/web/src/features/Providers/Providers.tsx b/web/src/features/Providers/Providers.tsx index 763c7f2..657d891 100644 --- a/web/src/features/Providers/Providers.tsx +++ b/web/src/features/Providers/Providers.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { BrowserRouter as Router } from "react-router-dom"; import { SuspenseProvider } from "@features/SuspenseProvider/SuspenseProvider"; import { ThemeProvider } from "@features/ThemeProvider/ThemeProvider"; @@ -9,9 +8,7 @@ export const Providers: React.FC = ({ children }) => { return ( - - {children} - + {children} ); diff --git a/web/src/features/SuspenseProvider/SuspenseProvider.tsx b/web/src/features/SuspenseProvider/SuspenseProvider.tsx index 7582246..8497252 100644 --- a/web/src/features/SuspenseProvider/SuspenseProvider.tsx +++ b/web/src/features/SuspenseProvider/SuspenseProvider.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { LinearProgress } from "@mui/material"; import { useStyles } from "./styles"; diff --git a/web/src/features/ThemeProvider/ThemeProvider.tsx b/web/src/features/ThemeProvider/ThemeProvider.tsx index f3ab6c3..5f33f61 100644 --- a/web/src/features/ThemeProvider/ThemeProvider.tsx +++ b/web/src/features/ThemeProvider/ThemeProvider.tsx @@ -2,7 +2,6 @@ import React from "react"; import { createTheme, CssBaseline, Theme, useMediaQuery } from "@mui/material"; import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; - import { getPalette } from "@themes/palette"; export interface ThemeContextValue { diff --git a/web/src/features/UserProvider/UserProvider.tsx b/web/src/features/UserProvider/UserProvider.tsx index 9190144..670ffda 100644 --- a/web/src/features/UserProvider/UserProvider.tsx +++ b/web/src/features/UserProvider/UserProvider.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from "react"; -import { Auth, Hub } from "aws-amplify"; import { User } from "@interfaces/User"; import { HubEvent } from "@janush-types/enums/HubEvent"; import { Nullable } from "@janush-types/useful"; +import { Auth, Hub } from "aws-amplify"; export interface UserContextValue { user?: Nullable; diff --git a/web/src/index.tsx b/web/src/index.tsx index 5f6a44d..c1a7289 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,10 +1,11 @@ import React from "react"; + import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; -import reportWebVitals from "./reportWebVitals"; import { configureAws } from "./awsConfig"; +import reportWebVitals from "./reportWebVitals"; configureAws(); diff --git a/web/src/layouts/AuthLayout/AuthLayout.tsx b/web/src/layouts/AuthLayout/AuthLayout.tsx index 779cda3..ed1e283 100644 --- a/web/src/layouts/AuthLayout/AuthLayout.tsx +++ b/web/src/layouts/AuthLayout/AuthLayout.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { Box, Container, ContainerProps } from "@mui/material"; import { useStyles } from "./styles"; diff --git a/web/src/layouts/Logo/Logo.tsx b/web/src/layouts/Logo/Logo.tsx index 582bbb0..11c4ce9 100644 --- a/web/src/layouts/Logo/Logo.tsx +++ b/web/src/layouts/Logo/Logo.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { Typography } from "@mui/material"; import { Link } from "react-router-dom"; diff --git a/web/src/layouts/Modals/FormModalLayout/FormModalLayout.tsx b/web/src/layouts/Modals/FormModalLayout/FormModalLayout.tsx new file mode 100644 index 0000000..1a58be1 --- /dev/null +++ b/web/src/layouts/Modals/FormModalLayout/FormModalLayout.tsx @@ -0,0 +1,75 @@ +import { FC } from "react"; + +import { Button } from "@components/Button/Button"; +import CloseIcon from "@mui/icons-material/Close"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Theme, +} from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import { useTheme } from "@mui/material/styles"; +import { rgbaColors } from "@themes/palette"; + +interface Props { + isOpen: boolean; + title: string; + buttonTitle: string; + isButtonDisabled?: boolean; + onSubmit?: () => void; + onModalClose: () => void; +} + +export const FormModalLayout: FC = ({ + children, + isOpen, + title, + buttonTitle, + isButtonDisabled = false, + onSubmit, + onModalClose, +}) => { + const theme = useTheme(); + + return ( + + + {title} + + {children} + + + + + + + + ); +}; diff --git a/web/src/layouts/PageLayout/PageLayout.test.tsx b/web/src/layouts/PageLayout/PageLayout.test.tsx index 4bdbf78..f1a53f5 100644 --- a/web/src/layouts/PageLayout/PageLayout.test.tsx +++ b/web/src/layouts/PageLayout/PageLayout.test.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { render } from "@testing-library/react"; import { PageLayout } from "@layouts/PageLayout/PageLayout"; +import { render } from "@testing-library/react"; describe("", () => { it("should render", async () => { diff --git a/web/src/layouts/PageLayout/PageLayout.tsx b/web/src/layouts/PageLayout/PageLayout.tsx index 41fb75b..c29616b 100644 --- a/web/src/layouts/PageLayout/PageLayout.tsx +++ b/web/src/layouts/PageLayout/PageLayout.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { Box, BoxProps, Container, ContainerProps } from "@mui/material"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/web/src/layouts/TopAppBar/TopAppBar.tsx b/web/src/layouts/TopAppBar/TopAppBar.tsx index fd60b66..f2ba5d1 100644 --- a/web/src/layouts/TopAppBar/TopAppBar.tsx +++ b/web/src/layouts/TopAppBar/TopAppBar.tsx @@ -1,13 +1,17 @@ -import React from "react"; -import { AppBar, Toolbar, Button } from "@mui/material"; +import { VFC } from "react"; +import { useUserContext } from "@features/UserProvider/useUserContext"; import { Logo } from "@layouts/Logo/Logo"; +import { AppBar, Toolbar, Button } from "@mui/material"; +import { Paths } from "@routing/paths"; import { Auth } from "aws-amplify"; -import { useUserContext } from "@features/UserProvider/useUserContext"; import { NavLink } from "react-router-dom"; -import { Paths } from "@routing/paths"; -export const TopAppBar: React.VFC = () => { +interface Props { + showLogo?: boolean; +} + +export const TopAppBar: VFC = ({ showLogo = true }) => { const { user } = useUserContext(); const signOut = async () => { @@ -19,8 +23,8 @@ export const TopAppBar: React.VFC = () => { }; return ( - - + + {showLogo && } {user ? ( + {/* TODO: Handle removing user while implementing backend */} + + + + } + > + + + + + + + + + + + + setIsEditGroupModalOpen(false)} + variant={GroupModalVariant.Edit} + /> + setIsDeleteGroupModalOpen(false)} + // TODO: Add function for submitting behavior while backend implementation + onSubmit={() => null} + submitButtonTitle="Remove user" + > + Deleting group + + Are you sure you want to delete this group? + + + + ); +}; + +export default GroupDetails; diff --git a/web/src/routing/routes/UsersAdministration/Groups/Groups.tsx b/web/src/routing/routes/UsersAdministration/Groups/Groups.tsx new file mode 100644 index 0000000..2461382 --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Groups/Groups.tsx @@ -0,0 +1,49 @@ +import { VFC, useState } from "react"; + +import { Button } from "@components/Button/Button"; +import { Group } from "@janush-types/group"; +import { UsersAdministrationLayout } from "@layouts/UsersAdministrationLayout/UsersAdministrationLayout"; + +import GroupsTable from "./GroupsTable/GroupsTable"; +import { GroupModal, GroupModalVariant } from "./Modals/GroupModal/GroupModal"; + +// TODO: Remove it after getting data from backend +const data: Group[] = [ + { + id: "1c4a9a8d-b652-421a-b130-9ad680029521", + name: "Group0001", + description: "Admins", + members: 1, + lastModified: "15-06-2021", + }, + { + id: "1c4a9a8d-b652-421a-b130-9ad680029522", + name: "Group0002", + description: "Moderators", + members: 3, + lastModified: "15-06-2021", + }, +]; + +const Groups: VFC = () => { + const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false); + + return ( + setIsCreateGroupModalOpen(true)}> + New Group + + } + > + + setIsCreateGroupModalOpen(false)} + variant={GroupModalVariant.Create} + /> + + ); +}; + +export default Groups; diff --git a/web/src/routing/routes/UsersAdministration/Groups/GroupsTable/GroupsTable.tsx b/web/src/routing/routes/UsersAdministration/Groups/GroupsTable/GroupsTable.tsx new file mode 100644 index 0000000..d50e133 --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Groups/GroupsTable/GroupsTable.tsx @@ -0,0 +1,73 @@ +import { VFC } from "react"; + +import { Table } from "@components/Table/Table"; +import { Group } from "@janush-types/group"; +import { Theme } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { rgbaColors } from "@themes/palette"; +import { useNavigate } from "react-router-dom"; + +const columns = [ + { name: "name", label: "Name" }, + { name: "description", label: "Description" }, + { name: "members", label: "Members" }, + { name: "lastModified", label: "Last Modified" }, +]; + +interface Props { + data: Group[]; +} + +const { TableHead, TableRow, TableCell, TableBody } = Table; + +const GroupsTable: VFC = ({ data }) => { + const navigate = useNavigate(); + const theme = useTheme(); + + const onRowClick = (group: Group) => { + navigate(group.id, { state: { group } }); + }; + + return ( + + + + {columns.map((column) => ( + {column.label} + ))} + + + + {data.length ? ( + data.map((item) => ( + onRowClick(item)} + > + {item.name} + {item.description} + {item.members} + + {item.lastModified} + + + )) + ) : ( + + + No results + + + )} + +
+ ); +}; + +export default GroupsTable; diff --git a/web/src/routing/routes/UsersAdministration/Groups/Modals/AddToGroupModal/AddToGroupModal.tsx b/web/src/routing/routes/UsersAdministration/Groups/Modals/AddToGroupModal/AddToGroupModal.tsx new file mode 100644 index 0000000..d8fc2dd --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Groups/Modals/AddToGroupModal/AddToGroupModal.tsx @@ -0,0 +1,69 @@ +import { VFC } from "react"; + +import { Select } from "@components/Select/Select"; +import { FormModalLayout } from "@layouts/Modals/FormModalLayout/FormModalLayout"; +import { Box, Typography, Theme } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { useForm, Controller } from "react-hook-form"; + +const groupOptions = [ + { name: "group1", label: "Group1" }, + { name: "group2", label: "Group2" }, + { name: "group3", label: "Group3" }, +]; + +interface FormData { + group: string; +} + +interface Props { + isOpen: boolean; + onModalClose: () => void; +} + +export const AddToGroupModal: VFC = ({ ...props }) => { + const theme = useTheme(); + + const { control, formState, handleSubmit } = useForm({ + mode: "onChange", + defaultValues: { + group: "group1", + }, + }); + + return ( + + + b.szurek@codeandpepper.com + + + Group + ( + + )} + /> + + + ); +}; diff --git a/web/src/routing/routes/UsersAdministration/Users/Modals/UserModal/UserModal.tsx b/web/src/routing/routes/UsersAdministration/Users/Modals/UserModal/UserModal.tsx new file mode 100644 index 0000000..2fb19f8 --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Users/Modals/UserModal/UserModal.tsx @@ -0,0 +1,149 @@ +import { VFC } from "react"; + +import { FormInput } from "@components/FormInput/FormInput"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FormModalLayout } from "@layouts/Modals/FormModalLayout/FormModalLayout"; +import { Box, Checkbox, FormControlLabel } from "@mui/material"; +import { emailSchema } from "@validations/UserValidation"; +import { useForm, Controller } from "react-hook-form"; + +export enum UserModalVariant { + Create = "create", + Edit = "edit", +} + +interface FormData { + email: string; + phoneNumber: string; + isEmailVerified: boolean; + password: string; + shouldResendVerificationEmail: boolean; + shouldResendInvitationEmail: boolean; +} + +interface Props { + isOpen: boolean; + onModalClose: () => void; + variant: UserModalVariant; +} + +export const UserModal: VFC = ({ variant, ...restProps }) => { + const { control, formState, handleSubmit } = useForm({ + mode: "onChange", + resolver: yupResolver(emailSchema), + defaultValues: { + email: "", + phoneNumber: "", + isEmailVerified: true, + password: "", + shouldResendVerificationEmail: false, + shouldResendInvitationEmail: false, + }, + }); + + return ( + + ( + + )} + /> + ( + + )} + /> + {variant === UserModalVariant.Edit && ( + ( + + )} + /> + )} + {variant === UserModalVariant.Create && ( + ( + } + label="Mark email as verified" + sx={{ + mt: 2, + "& .MuiFormControlLabel-label": { + fontSize: "12px", + }, + }} + /> + )} + /> + )} + {variant === UserModalVariant.Edit && ( + + ( + } + label="Resend verification email" + sx={{ + ml: 1, + mt: 2, + "& .MuiFormControlLabel-label": { + fontSize: "12px", + }, + }} + /> + )} + /> + ( + } + label="Resend invitation email" + sx={{ + ml: 1, + "& .MuiFormControlLabel-label": { + fontSize: "12px", + }, + }} + /> + )} + /> + + )} + + ); +}; diff --git a/web/src/routing/routes/UsersAdministration/Users/UserDetails/UserDetails.tsx b/web/src/routing/routes/UsersAdministration/Users/UserDetails/UserDetails.tsx new file mode 100644 index 0000000..58889b5 --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Users/UserDetails/UserDetails.tsx @@ -0,0 +1,114 @@ +import { useState, VFC, FC } from "react"; + +import { Button } from "@components/Button/Button"; +import { ConfirmationDialog } from "@components/ConfirmationDialog/ConfirmationDialog"; +import { ListElement } from "@components/ListElement/ListElement"; +import { User } from "@janush-types/user"; +import { UsersAdministrationLayout } from "@layouts/UsersAdministrationLayout/UsersAdministrationLayout"; +import { Box, Typography, Theme, ButtonProps } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { AddToGroupModal } from "@routing/routes/UsersAdministration/Groups/Modals/AddToGroupModal/AddToGroupModal"; +import { rgbaColors } from "@themes/palette"; +import { useLocation, useNavigate } from "react-router-dom"; + +import { UserModal, UserModalVariant } from "../Modals/UserModal/UserModal"; + +interface LocationState { + user: User; +} + +const ButtonWrapper: FC = ({ children, ...buttonProps }) => ( + +); + +const UserDetails: VFC = () => { + const theme = useTheme(); + const navigate = useNavigate(); + + const location = useLocation(); + // TODO: change to get user data from backend + const { user } = location.state as LocationState; + + const [isEditUserModalOpen, setIsEditUserModalOpen] = useState(false); + const [isAddToGroupModalOpen, setIsAddToGroupModalOpen] = useState(false); + const [isRemoveUserModalOpen, setIsRemoveUserModalOpen] = useState(false); + + return ( + navigate(-1)} + buttons={ + <> + setIsEditUserModalOpen(true)}> + Edit + + setIsAddToGroupModalOpen(true)}> + Add to group + + {/* TODO: Handle enable button while implementing backend */} + Enable + {/* TODO: Handle disable button while implementing backend */} + Disable + setIsRemoveUserModalOpen(true)}> + Remove + + + } + > + + + Account Details + + + + + + + + + {/* TODO: set value from server */} + + + setIsEditUserModalOpen(false)} + variant={UserModalVariant.Edit} + /> + setIsAddToGroupModalOpen(false)} + /> + setIsRemoveUserModalOpen(false)} + // TODO: Add function for submitting behavior while backend implementation + onSubmit={() => null} + submitButtonTitle="Remove user" + > + Removing user + + Are you sure you want to remove this user? + + + + ); +}; + +export default UserDetails; diff --git a/web/src/routing/routes/UsersAdministration/Users/Users.tsx b/web/src/routing/routes/UsersAdministration/Users/Users.tsx new file mode 100644 index 0000000..a21d409 --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Users/Users.tsx @@ -0,0 +1,209 @@ +import { useState, VFC } from "react"; + +import { Button } from "@components/Button/Button"; +import { SearchIcon } from "@components/icons/SearchIcon/SearchIcon"; +import { Select } from "@components/Select/Select"; +import { User } from "@janush-types/user"; +import { UsersAdministrationLayout } from "@layouts/UsersAdministrationLayout/UsersAdministrationLayout"; +import { Box, Typography } from "@mui/material"; +import Input from "@mui/material/Input"; +import InputAdornment from "@mui/material/InputAdornment"; +import { rgbaColors } from "@themes/palette"; +import { getOptionsFromEnum } from "@utils/getOptionsFromEnum/getOptionsFromEnum"; +import { useNavigate } from "react-router-dom"; + +import { UserModal, UserModalVariant } from "./Modals/UserModal/UserModal"; +import UsersTable from "./UsersTable/UsersTable"; + +enum SearchBy { + ID = "id", + Email = "email", + Access = "access", + Status = "status", +} + +enum Status { + All = "all", + Confirmed = "confirmed", + Unconfirmed = "unconfirmed", +} + +enum Access { + All = "all", + Enabled = "enabled", + Disabled = "disabled", +} + +interface Search { + searchBy: SearchBy; + searchFor: string; +} + +// TODO: Remove it after getting data from backend +const data: User[] = [ + { + id: "1c4a9a8d-b652-421a-b130-9ad680029521", + email: "b.szurek@codeandpepper.com", + phoneNumber: "+48 666 777 888", + access: true, + status: true, + lastModified: "15-06-2021", + }, + { + id: "1c4a9a8d-b652-421a-b130-9ad680029522", + email: "b.szurek@codeandpepper.com", + phoneNumber: "+48 666 777 888", + access: false, + status: false, + lastModified: "15-06-2021", + }, + { + id: "1c4a9a8d-b652-421a-b130-9ad680029523", + email: "b.szurek@codeandpepper.com", + phoneNumber: "+48 666 777 888", + access: true, + status: false, + lastModified: "15-06-2021", + }, +]; + +const Users: VFC = () => { + const [search, setSearch] = useState({ + searchBy: SearchBy.Email, + searchFor: "", + }); + const [status, setStatus] = useState(Status.All); + const [access, setAccess] = useState(Access.All); + const [isCreateUserModalOpen, setIsCreateUserModalOpen] = useState(false); + + const navigate = useNavigate(); + + const onTableRowClick = (user: User) => { + navigate(user.id, { state: { user } }); + }; + + return ( + + + + + + + } + > + + + + + + + } + sx={{ + pl: 2, + pr: 1.5, + // TODO: Static width need to be removed while implementing RWD + width: 272, + "& .MuiInput-input": { + pb: 1, + }, + }} + /> + )} + {search.searchBy === SearchBy.Status && ( + setAccess(e.target.value as Access)} + options={getOptionsFromEnum(Access)} + sx={{ + // TODO: Static width need to be removed while implementing RWD + width: 272, + "& .MuiSelect-select": { + pl: 2, + }, + "& .MuiSvgIcon-root": { + mr: 1.5, + }, + }} + /> + )} + + + {`Showing ${data.length} of ${data.length}`} + {` (selected 0)`} + + + + + setIsCreateUserModalOpen(false)} + variant={UserModalVariant.Create} + /> + + ); +}; + +export default Users; diff --git a/web/src/routing/routes/UsersAdministration/Users/UsersTable/UsersTable.tsx b/web/src/routing/routes/UsersAdministration/Users/UsersTable/UsersTable.tsx new file mode 100644 index 0000000..8707dbc --- /dev/null +++ b/web/src/routing/routes/UsersAdministration/Users/UsersTable/UsersTable.tsx @@ -0,0 +1,102 @@ +import { VFC, FC } from "react"; + +import { Table } from "@components/Table/Table"; +import { User } from "@janush-types/user"; +import { Typography, Theme, Checkbox } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { rgbaColors } from "@themes/palette"; + +const columns = [ + { name: "checkbox", label: "" }, + { name: "id", label: "ID" }, + { name: "email", label: "Email" }, + { name: "phoneNumber", label: "Phone Number" }, + { name: "access", label: "Access" }, + { name: "status", label: "Status" }, + { name: "lastmodified", label: "Last Modified" }, +]; + +interface Props { + data: User[]; + onRowClick: (user: User) => void; +} + +const { TableHead, TableRow, TableCell, TableBody } = Table; + +const TableCellStyled: FC = ({ children }) => ( + {children} +); + +const UsersTable: VFC = ({ data, onRowClick }) => { + const theme = useTheme(); + + return ( + + + + {columns.map((column) => ( + {column.label} + ))} + + + + {data.length ? ( + data.map((item) => ( + onRowClick(item)} + > + + e.stopPropagation()} /> + + {item.id} + {item.email} + {item.phoneNumber} + + + {item.access ? "Enabled" : "Disabled"} + + + + + {item.access ? "Confirmed" : "Unconfirmed"} + + + {item.lastModified} + + )) + ) : ( + + + No results + + + )} + +
+ ); +}; + +export default UsersTable; diff --git a/web/src/routing/routes/VerifyEmail/VerifyEmail.test.tsx b/web/src/routing/routes/VerifyEmail/VerifyEmail.test.tsx index d691da7..4cb2915 100644 --- a/web/src/routing/routes/VerifyEmail/VerifyEmail.test.tsx +++ b/web/src/routing/routes/VerifyEmail/VerifyEmail.test.tsx @@ -1,4 +1,7 @@ import React from "react"; + +import { ThemeProvider } from "@features/ThemeProvider/ThemeProvider"; +import { Paths } from "@routing/paths"; import { act, fireEvent, @@ -7,12 +10,10 @@ import { waitFor, } from "@testing-library/react"; import { Auth } from "aws-amplify"; +import { MemoryHistory } from "history"; import { MemoryRouter, Route, Router } from "react-router-dom"; -import { ThemeProvider } from "@features/ThemeProvider/ThemeProvider"; import VerifyEmail from "./VerifyEmail"; -import { Paths } from "@routing/paths"; -import { MemoryHistory } from "history"; const setupHistory: MemoryHistory = { push: jest.fn(), diff --git a/web/src/routing/routes/VerifyEmail/VerifyEmail.tsx b/web/src/routing/routes/VerifyEmail/VerifyEmail.tsx index c5dfd01..f59160b 100644 --- a/web/src/routing/routes/VerifyEmail/VerifyEmail.tsx +++ b/web/src/routing/routes/VerifyEmail/VerifyEmail.tsx @@ -1,15 +1,15 @@ import React, { useState } from "react"; -import { Box, Button, Container, Grid, Typography } from "@mui/material"; -import { Auth } from "aws-amplify"; -import { Helmet } from "react-helmet"; -import { Redirect, useLocation } from "react-router-dom"; import { Link } from "@components/Link/Link"; import { AuthLayout } from "@layouts/AuthLayout/AuthLayout"; +import { Box, Button, Container, Grid, Typography } from "@mui/material"; import { Paths } from "@routing/paths"; +import { VerifyEmailForm } from "@routing/routes/VerifyEmail/VerifyEmailView/VerifyEmailForm"; +import { Auth } from "aws-amplify"; +import { Helmet } from "react-helmet"; +import { useLocation, Navigate } from "react-router-dom"; import { useStyles } from "./styles"; -import { VerifyEmailForm } from "@routing/routes/VerifyEmail/VerifyEmailView/VerifyEmailForm"; interface LocationState { email?: string; @@ -17,7 +17,9 @@ interface LocationState { const VerifyEmail: React.VFC = () => { const classes = useStyles(); - const { state } = useLocation(); + + const location = useLocation(); + const state = location.state as LocationState; const [disabled, setDisabled] = useState(false); @@ -31,7 +33,7 @@ const VerifyEmail: React.VFC = () => { } } - if (!state?.email) return ; + if (!state?.email) return ; return ( diff --git a/web/src/routing/routes/VerifyEmail/VerifyEmailView/VerifyEmailForm.tsx b/web/src/routing/routes/VerifyEmail/VerifyEmailView/VerifyEmailForm.tsx index 23b4e56..beaa61d 100644 --- a/web/src/routing/routes/VerifyEmail/VerifyEmailView/VerifyEmailForm.tsx +++ b/web/src/routing/routes/VerifyEmail/VerifyEmailView/VerifyEmailForm.tsx @@ -1,12 +1,13 @@ -import React, { useState, VFC } from "react"; +import { useState, VFC } from "react"; + +import { Form } from "@components/Form/Form"; import { TextField } from "@components/TextField/TextField"; import { Button } from "@mui/material"; -import { Form } from "@components/Form/Form"; -import { Controller, useForm } from "react-hook-form"; -import { Auth } from "aws-amplify"; -import { useHistory } from "react-router-dom"; import { Paths } from "@routing/paths"; import { isCognitoError } from "@utils/isCognitoError/isCognitoError"; +import { Auth } from "aws-amplify"; +import { Controller, useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; interface IProps { email: string; } @@ -16,7 +17,7 @@ interface IVerifyEmailState { } export const VerifyEmailForm: VFC = ({ email }) => { - const history = useHistory(); + const navigate = useNavigate(); const [message, setMessage] = useState(""); const { handleSubmit, control } = useForm(); @@ -24,7 +25,7 @@ export const VerifyEmailForm: VFC = ({ email }) => { try { await Auth.confirmSignUp(email, code); - history.push(Paths.SIGN_IN_PATH); + navigate(Paths.SIGN_IN_PATH); } catch (err: unknown) { if (isCognitoError(err)) { setMessage(err.message); diff --git a/web/src/themes/palette.ts b/web/src/themes/palette.ts index 9420b30..eb0ea38 100644 --- a/web/src/themes/palette.ts +++ b/web/src/themes/palette.ts @@ -5,6 +5,13 @@ const lightPalette = createPalette({ main: "#3F51B5", light: "#B6BDE3", }, + secondary: { + main: "#F7F7F7", + dark: "#666666", + }, + error: { + main: "#B00020", + }, }); const darkPalette = createPalette({ @@ -12,7 +19,25 @@ const darkPalette = createPalette({ main: "#3F51B5", light: "#B6BDE3", }, + secondary: { + main: "#F7F7F7", + dark: "#666666", + }, + error: { + main: "#B00020", + }, }); export const getPalette = (darkMode: boolean): Palette => darkMode ? darkPalette : lightPalette; + +export const rgbaColors = { + grey: { + lightest: "rgba(0, 0, 0, 0.05)", + lighter: "rgba(0, 0, 0, 0.12)", + light: "rgba(0, 0, 0, 0.15)", + main: "rgba(0, 0, 0, 0.38)", + dark: "rgba(0, 0, 0, 0.6)", + darkest: "rgba(63, 81, 181, 0.05)", + }, +}; diff --git a/web/src/types/group.ts b/web/src/types/group.ts new file mode 100644 index 0000000..8003403 --- /dev/null +++ b/web/src/types/group.ts @@ -0,0 +1,7 @@ +export interface Group { + id: string; + name: string; + description: string; + members: number; + lastModified: string; +} diff --git a/web/src/types/user.ts b/web/src/types/user.ts new file mode 100644 index 0000000..122dbc3 --- /dev/null +++ b/web/src/types/user.ts @@ -0,0 +1,8 @@ +export interface User { + id: string; + email: string; + phoneNumber: string; + access: boolean; + status: boolean; + lastModified: string; +} diff --git a/web/src/utils/checkIsThisCurrentTab/checkIsThisCurrentTab.ts b/web/src/utils/checkIsThisCurrentTab/checkIsThisCurrentTab.ts new file mode 100644 index 0000000..032bfe2 --- /dev/null +++ b/web/src/utils/checkIsThisCurrentTab/checkIsThisCurrentTab.ts @@ -0,0 +1,6 @@ +import { Location } from "react-router-dom"; + +export const checkIsThisCurrentTab = ( + location: Location, + tabName: string +): boolean => location.pathname.split("/")[2] === tabName; diff --git a/web/src/utils/getOptionsFromEnum/getOptionsFromEnum.ts b/web/src/utils/getOptionsFromEnum/getOptionsFromEnum.ts new file mode 100644 index 0000000..86591b8 --- /dev/null +++ b/web/src/utils/getOptionsFromEnum/getOptionsFromEnum.ts @@ -0,0 +1,5 @@ +export const getOptionsFromEnum = (passedEnum: any) => + Object.keys(passedEnum).map((item) => ({ + name: item.toLocaleLowerCase(), + label: item, + })); diff --git a/web/src/utils/isCognitoError/isCognitoError.test.ts b/web/src/utils/isCognitoError/isCognitoError.test.ts index 73eb00b..44a507e 100644 --- a/web/src/utils/isCognitoError/isCognitoError.test.ts +++ b/web/src/utils/isCognitoError/isCognitoError.test.ts @@ -1,4 +1,5 @@ import { CognitoErrorType } from "@janush-types/enums/Cognito"; + import { isCognitoError } from "./isCognitoError"; describe("isCognitoError", () => { diff --git a/web/src/validations/UserValidation.ts b/web/src/validations/UserValidation.ts new file mode 100644 index 0000000..079da83 --- /dev/null +++ b/web/src/validations/UserValidation.ts @@ -0,0 +1,11 @@ +import * as yup from "yup"; + +export const baseSchema = { + firstName: yup.string().max(30).required().label("First name"), + lastName: yup.string().max(30).required().label("Last name"), +}; + +export const emailSchema = yup.object({ + ...baseSchema, + email: yup.string().email().required().label("Email address"), +}); diff --git a/web/tsconfig.paths.json b/web/tsconfig.paths.json index b46c425..540e56d 100644 --- a/web/tsconfig.paths.json +++ b/web/tsconfig.paths.json @@ -2,15 +2,36 @@ "compilerOptions": { "baseUrl": ".", "paths": { -"@components/*": ["./src/components/*"], - "@consts/*": ["./src/consts/*"], - "@interfaces/*": ["./src/interfaces/*"], - "@janush-types/*": ["./src/types/*"], - "@utils/*": ["./src/utils/*"], - "@features/*": ["./src/features/*"], - "@layouts/*": ["./src/layouts/*"], - "@routing/*": ["./src/routing/*"], - "@themes/*": ["./src/themes/*"], + "@components/*": [ + "./src/components/*" + ], + "@consts/*": [ + "./src/consts/*" + ], + "@interfaces/*": [ + "./src/interfaces/*" + ], + "@janush-types/*": [ + "./src/types/*" + ], + "@utils/*": [ + "./src/utils/*" + ], + "@features/*": [ + "./src/features/*" + ], + "@layouts/*": [ + "./src/layouts/*" + ], + "@routing/*": [ + "./src/routing/*" + ], + "@themes/*": [ + "./src/themes/*" + ], + "@validations/*": [ + "./src/validations/*" + ], } } -} +} \ No newline at end of file