From 6c92dff0d4d07944bb8f47b6499ad8bb0356d62a Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 6 Sep 2021 20:04:08 +1000 Subject: [PATCH] rewrite home page, add useBasicSearch --- package.json | 3 +- src/components/AppBar.tsx | 76 --- src/components/FloatingSearch.tsx | 36 +- src/components/Home/AccessDenied.tsx | 51 ++ .../{ => Home}/HomeWelcomePrompt.tsx | 8 +- src/components/Home/TableGrid/TableCard.tsx | 76 +++ .../Home/TableGrid/TableCardSkeleton.tsx | 42 ++ .../Home/TableGrid/TableGridSkeleton.tsx | 48 ++ src/components/Home/TableGrid/index.tsx | 73 +++ .../Home/TableList/TableListItem.tsx | 78 +++ .../Home/TableList/TableListItemSkeleton.tsx | 23 + .../Home/TableList/TableListSkeleton.tsx | 36 ++ src/components/Home/TableList/index.tsx | 71 +++ src/components/Modal/SlideTransition.tsx | 4 +- src/components/Navigation/NavItem.tsx | 1 + src/components/Navigation/NavTableSection.tsx | 6 +- src/components/Navigation/UserMenu.tsx | 2 +- src/components/RichTextEditor.tsx | 9 +- src/components/SectionHeading.tsx | 70 +++ src/components/SectionHeadingSkeleton.tsx | 14 + src/components/Settings/SettingsSection.tsx | 47 +- .../Settings/UserManagement/InviteUser.tsx | 81 +++ .../Settings/UserManagement/UserItem.tsx | 12 +- src/components/StyledCard.tsx | 181 ------- src/contexts/AppContext.tsx | 2 +- src/contexts/RowyContext.tsx | 5 +- src/hooks/useBasicSearch.ts | 17 + src/pages/Home.tsx | 473 +++++++----------- src/pages/Settings/ProjectSettings.tsx | 8 +- src/pages/Settings/UserManagement.tsx | 94 ++-- src/pages/Settings/UserSettings.tsx | 2 +- src/theme/colors.ts | 11 +- src/theme/components.tsx | 72 +-- yarn.lock | 24 +- 34 files changed, 1078 insertions(+), 678 deletions(-) delete mode 100644 src/components/AppBar.tsx create mode 100644 src/components/Home/AccessDenied.tsx rename src/components/{ => Home}/HomeWelcomePrompt.tsx (85%) create mode 100644 src/components/Home/TableGrid/TableCard.tsx create mode 100644 src/components/Home/TableGrid/TableCardSkeleton.tsx create mode 100644 src/components/Home/TableGrid/TableGridSkeleton.tsx create mode 100644 src/components/Home/TableGrid/index.tsx create mode 100644 src/components/Home/TableList/TableListItem.tsx create mode 100644 src/components/Home/TableList/TableListItemSkeleton.tsx create mode 100644 src/components/Home/TableList/TableListSkeleton.tsx create mode 100644 src/components/Home/TableList/index.tsx create mode 100644 src/components/SectionHeading.tsx create mode 100644 src/components/SectionHeadingSkeleton.tsx create mode 100644 src/components/Settings/UserManagement/InviteUser.tsx delete mode 100644 src/components/StyledCard.tsx create mode 100644 src/hooks/useBasicSearch.ts diff --git a/package.json b/package.json index 86504852..c5605d5d 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "react-joyride": "^2.3.0", "react-json-view": "^1.19.1", "react-router-dom": "^5.0.1", + "react-router-hash-link": "^2.4.3", "react-scripts": "^4.0.3", "react-scroll-sync": "^0.8.0", - "react-use-search": "^0.3.1", "react-usestateref": "^1.0.5", "serve": "^11.3.2", "tinymce": "^5.2.0", @@ -106,6 +106,7 @@ "@types/react-div-100vh": "^0.3.0", "@types/react-dom": "^17.0.8", "@types/react-router-dom": "^5.1.7", + "@types/react-router-hash-link": "^2.4.1", "@types/use-persisted-state": "^0.3.0", "craco-alias": "^3.0.1", "firebase-tools": "^8.12.1", diff --git a/src/components/AppBar.tsx b/src/components/AppBar.tsx deleted file mode 100644 index a2d9aa1c..00000000 --- a/src/components/AppBar.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Link } from "react-router-dom"; - -import { makeStyles, createStyles } from "@material-ui/styles"; -import { - useTheme, - useScrollTrigger, - AppBar as MuiAppBar, - Toolbar, - Grid, - Button, -} from "@material-ui/core"; - -import Logo from "assets/Logo"; -import routes from "constants/routes"; - -const useStyles = makeStyles((theme) => - createStyles({ - appBar: { - backgroundColor: theme.palette.background.paper, - marginBottom: theme.spacing(6), - }, - - logo: { - display: "block", - marginRight: theme.spacing(1), - }, - heading: { - textTransform: "none", - color: theme.palette.primary.main, - cursor: "default", - userSelect: "none", - fontFeatureSettings: '"liga"', - }, - - locationDropdown: { - minWidth: 140, - margin: 0, - }, - }) -); - -interface IAppBarProps {} - -const AppBar: React.FunctionComponent = () => { - const classes = useStyles(); - const theme = useTheme(); - const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 }); - - return ( - - - - - - - - - - - - ); -}; - -export default AppBar; diff --git a/src/components/FloatingSearch.tsx b/src/components/FloatingSearch.tsx index ed4815d6..8cfcf460 100644 --- a/src/components/FloatingSearch.tsx +++ b/src/components/FloatingSearch.tsx @@ -7,12 +7,16 @@ import { } from "@material-ui/core"; import SearchIcon from "@material-ui/icons/Search"; +import { APP_BAR_HEIGHT } from "components/Navigation"; + export interface IFloatingSearchProps extends Partial { label: string; + paperSx?: FilledTextFieldProps["sx"]; } export default function FloatingSearch({ label, + paperSx, ...props }: IFloatingSearchProps) { const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 }); @@ -22,8 +26,9 @@ export default function FloatingSearch({ elevation={trigger ? 8 : 1} sx={{ position: "sticky", - top: (theme) => theme.spacing(7 + 1), + top: (theme) => theme.spacing(APP_BAR_HEIGHT / 8 + 1), zIndex: "appBar", + ...paperSx, }} > (theme.shape.borderRadius as number) * 4, + + boxShadow: (theme) => + `0 -1px 0 0 ${theme.palette.text.disabled} inset`, + "&:hover": { + boxShadow: (theme) => + `0 -1px 0 0 ${theme.palette.text.primary} inset`, + }, + "&.Mui-focused, &.Mui-focused:hover": { + boxShadow: (theme) => + `0 -2px 0 0 ${theme.palette.primary.main} inset`, + }, + + "&::after": { + width: (theme) => + `calc(100% - ${ + (theme.shape.borderRadius as number) * 2 * 2 + }px)`, + left: (theme) => (theme.shape.borderRadius as number) * 2, }, }, }} diff --git a/src/components/Home/AccessDenied.tsx b/src/components/Home/AccessDenied.tsx new file mode 100644 index 00000000..656025fc --- /dev/null +++ b/src/components/Home/AccessDenied.tsx @@ -0,0 +1,51 @@ +import { Link } from "react-router-dom"; + +import { Typography, Link as MuiLink, Button } from "@material-ui/core"; +import SecurityIcon from "@material-ui/icons/SecurityOutlined"; + +import EmptyState from "components/EmptyState"; + +import WIKI_LINKS from "constants/wikiLinks"; +import routes from "constants/routes"; + +export default function AccessDenied() { + return ( + + + You do not have access to this project. Please contact the project + owner. + + + If you are the project owner, please follow{" "} + + these instructions + {" "} + to set up the project’s security rules. + + + + + } + sx={{ + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + bgcolor: "background.default", + zIndex: 9999, + }} + /> + ); +} diff --git a/src/components/HomeWelcomePrompt.tsx b/src/components/Home/HomeWelcomePrompt.tsx similarity index 85% rename from src/components/HomeWelcomePrompt.tsx rename to src/components/Home/HomeWelcomePrompt.tsx index a16b2f1f..a2d9fbfe 100644 --- a/src/components/HomeWelcomePrompt.tsx +++ b/src/components/Home/HomeWelcomePrompt.tsx @@ -2,13 +2,7 @@ import { Zoom, Stack, Typography } from "@material-ui/core"; export default function HomeWelcomePrompt() { return ( - + + + + + {section} + + + {name} + + + (theme.typography.body2.lineHeight as number) * 2 + "em", + }} + > + {description} + + + + + + + +
+ + {actions} + + + ); +} diff --git a/src/components/Home/TableGrid/TableCardSkeleton.tsx b/src/components/Home/TableGrid/TableCardSkeleton.tsx new file mode 100644 index 00000000..08645003 --- /dev/null +++ b/src/components/Home/TableGrid/TableCardSkeleton.tsx @@ -0,0 +1,42 @@ +import { + Card, + CardContent, + Typography, + CardActions, + Skeleton, +} from "@material-ui/core"; + +export default function TableCardSkeleton() { + return ( + + + + + + + + + + (theme.typography.body2.lineHeight as number) * 2 + "em", + }} + > + + + + + + + + + + + ); +} diff --git a/src/components/Home/TableGrid/TableGridSkeleton.tsx b/src/components/Home/TableGrid/TableGridSkeleton.tsx new file mode 100644 index 00000000..fcdb257f --- /dev/null +++ b/src/components/Home/TableGrid/TableGridSkeleton.tsx @@ -0,0 +1,48 @@ +import { Container, Paper, Box, Grid } from "@material-ui/core"; + +import SectionHeadingSkeleton from "components/SectionHeadingSkeleton"; +import TableCardSkeleton from "./TableCardSkeleton"; + +export default function TableGridSkeleton() { + return ( + + theme.breakpoints.values.sm - 48, + width: { xs: "100%", md: "50%", lg: "100%" }, + mx: "auto", + }} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/Home/TableGrid/index.tsx b/src/components/Home/TableGrid/index.tsx new file mode 100644 index 00000000..c51325b3 --- /dev/null +++ b/src/components/Home/TableGrid/index.tsx @@ -0,0 +1,73 @@ +import { TransitionGroup } from "react-transition-group"; + +import { Box, Grid, Collapse } from "@material-ui/core"; + +import SectionHeading from "components/SectionHeading"; +import TableCard from "./TableCard"; +import SlideTransition from "components/Modal/SlideTransition"; + +import { Table } from "contexts/RowyContext"; + +export interface ITableGridProps { + sections: Record; + getLink: (table: Table) => string; + getActions?: (table: Table) => React.ReactNode; +} + +export default function TableGrid({ + sections, + getLink, + getActions, +}: ITableGridProps) { + return ( + + {Object.entries(sections).map( + ([sectionName, sectionTables], sectionIndex) => { + const tableItems = sectionTables + .map((table, tableIndex) => { + if (!table) return null; + + return ( + + + + + + ); + }) + .filter((item) => item !== null); + + if (tableItems.length === 0) return null; + + return ( + + + + + {sectionName} + + + + + {tableItems} + + + + ); + } + )} + + ); +} diff --git a/src/components/Home/TableList/TableListItem.tsx b/src/components/Home/TableList/TableListItem.tsx new file mode 100644 index 00000000..bcf143ea --- /dev/null +++ b/src/components/Home/TableList/TableListItem.tsx @@ -0,0 +1,78 @@ +import { Link } from "react-router-dom"; + +import { + ListItem, + ListItemButton, + Typography, + IconButton, +} from "@material-ui/core"; +import GoIcon from "@material-ui/icons/ArrowForward"; + +import { Table } from "contexts/RowyContext"; + +export interface ITableListItemProps extends Table { + link: string; + actions?: React.ReactNode; +} + +export default function TableListItem({ + // section, + name, + description, + link, + actions, +}: ITableListItemProps) { + return ( + + *": { lineHeight: "48px !important" }, + flexWrap: "nowrap", + overflow: "hidden", + }} + > + {/* + {section} + */} + + {name} + + + {description} + + + +
+ {actions} + + + + +
+
+ ); +} diff --git a/src/components/Home/TableList/TableListItemSkeleton.tsx b/src/components/Home/TableList/TableListItemSkeleton.tsx new file mode 100644 index 00000000..f8aa09c1 --- /dev/null +++ b/src/components/Home/TableList/TableListItemSkeleton.tsx @@ -0,0 +1,23 @@ +import { ListItem, Skeleton } from "@material-ui/core"; + +export default function TableListItemSkeleton() { + return ( + + + + + + + + ); +} diff --git a/src/components/Home/TableList/TableListSkeleton.tsx b/src/components/Home/TableList/TableListSkeleton.tsx new file mode 100644 index 00000000..f01e6bb6 --- /dev/null +++ b/src/components/Home/TableList/TableListSkeleton.tsx @@ -0,0 +1,36 @@ +import { Container, Box, Paper } from "@material-ui/core"; + +import SectionHeadingSkeleton from "components/SectionHeadingSkeleton"; +import TableListItemSkeleton from "./TableListItemSkeleton"; + +export default function TableGridSkeleton() { + return ( + + theme.breakpoints.values.sm - 48, + width: { xs: "100%", md: "50%", lg: "100%" }, + mx: "auto", + }} + /> + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/Home/TableList/index.tsx b/src/components/Home/TableList/index.tsx new file mode 100644 index 00000000..9343ba37 --- /dev/null +++ b/src/components/Home/TableList/index.tsx @@ -0,0 +1,71 @@ +import { TransitionGroup } from "react-transition-group"; + +import { Box, Paper, Collapse, List } from "@material-ui/core"; + +import SectionHeading from "components/SectionHeading"; +import TableListItem from "./TableListItem"; +import SlideTransition from "components/Modal/SlideTransition"; + +import { Table } from "contexts/RowyContext"; + +export interface ITableListProps { + sections: Record; + getLink: (table: Table) => string; + getActions?: (table: Table) => React.ReactNode; +} + +export default function TableList({ + sections, + getLink, + getActions, +}: ITableListProps) { + return ( + + {Object.entries(sections).map( + ([sectionName, sectionTables], sectionIndex) => { + const tableItems = sectionTables + .map((table) => { + if (!table) return null; + + return ( + + + + ); + }) + .filter((item) => item !== null); + + if (tableItems.length === 0) return null; + + return ( + + + + + {sectionName} + + + + + + + {tableItems} + + + + + + ); + } + )} + + ); +} diff --git a/src/components/Modal/SlideTransition.tsx b/src/components/Modal/SlideTransition.tsx index 242930d5..efcbc8d5 100644 --- a/src/components/Modal/SlideTransition.tsx +++ b/src/components/Modal/SlideTransition.tsx @@ -14,11 +14,11 @@ export const SlideTransition: React.ForwardRefExoticComponent< const defaultStyle = { opacity: 0, - transform: "translateY(16px)", + transform: "translateY(40px)", transition: theme.transitions.create(["transform", "opacity"], { duration: "300ms", - easing: "cubic-bezier(0.075, 0.82, 0.165, 1)", + easing: "cubic-bezier(0.1, 0.8, 0.1, 1)", }), }; diff --git a/src/components/Navigation/NavItem.tsx b/src/components/Navigation/NavItem.tsx index c4fc7a0f..2d670ba0 100644 --- a/src/components/Navigation/NavItem.tsx +++ b/src/components/Navigation/NavItem.tsx @@ -10,6 +10,7 @@ export default function NavItem(props: MenuItemProps) { selected={pathname === props.to} {...props} sx={{ + ...props.sx, "&&::before": { left: "auto", right: 0, diff --git a/src/components/Navigation/NavTableSection.tsx b/src/components/Navigation/NavTableSection.tsx index eb91fb15..1288b2cf 100644 --- a/src/components/Navigation/NavTableSection.tsx +++ b/src/components/Navigation/NavTableSection.tsx @@ -54,8 +54,12 @@ export default function NavDrawerItem({ )}` } onClick={closeDrawer} + sx={{ + ml: 2, + width: (theme) => `calc(100% - ${theme.spacing(2 + 0.5)})`, + }} > - + ))} diff --git a/src/components/Navigation/UserMenu.tsx b/src/components/Navigation/UserMenu.tsx index f6647539..0ea75573 100644 --- a/src/components/Navigation/UserMenu.tsx +++ b/src/components/Navigation/UserMenu.tsx @@ -33,7 +33,7 @@ export default function UserMenu(props: IconButtonProps) { setThemeOverridden, } = useAppContext(); if (!currentUser || !userDoc || !userDoc?.state?.doc) - return
; + return
; const displayName = userDoc?.state?.doc?.user?.displayName; const avatarUrl = userDoc?.state?.doc?.user?.photoURL; diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index 53ec5e86..d635555f 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -52,10 +52,11 @@ const useStyles = makeStyles((theme) => margin: 1, }, - "& .tox-toolbar-overlord, & .tox-edit-area__iframe, & .tox-toolbar__primary": { - background: "transparent", - borderRadius: (theme.shape.borderRadius as number) - 1, - }, + "& .tox-toolbar-overlord, & .tox-edit-area__iframe, & .tox-toolbar__primary": + { + background: "transparent", + borderRadius: (theme.shape.borderRadius as number) - 1, + }, "& .tox-toolbar__primary": { padding: theme.spacing(0.5, 0) }, "& .tox-toolbar__group": { diff --git a/src/components/SectionHeading.tsx b/src/components/SectionHeading.tsx new file mode 100644 index 00000000..5678ddc6 --- /dev/null +++ b/src/components/SectionHeading.tsx @@ -0,0 +1,70 @@ +import { forwardRef } from "react"; +import _camelCase from "lodash/camelCase"; +import { HashLink } from "react-router-hash-link"; + +import { Stack, StackProps, Typography, IconButton } from "@material-ui/core"; +import LinkIcon from "@material-ui/icons/Link"; + +import { APP_BAR_HEIGHT } from "components/Navigation"; + +export interface ISectionHeadingProps extends Omit { + children: string; +} + +export const SectionHeading = forwardRef(function SectionHeading_( + { children, sx, ...props }: ISectionHeadingProps, + ref +) { + const sectionLink = _camelCase(children); + + return ( + theme.spacing(APP_BAR_HEIGHT / 8 + 3.5), + scrollBehavior: "smooth", + }} + > + + {children} + + + theme.transitions.create("opacity", { + duration: theme.transitions.duration.short, + }), + + "&:focus": { opacity: 1 }, + }} + > + + + + ); +}); + +export default SectionHeading; diff --git a/src/components/SectionHeadingSkeleton.tsx b/src/components/SectionHeadingSkeleton.tsx new file mode 100644 index 00000000..cc8be045 --- /dev/null +++ b/src/components/SectionHeadingSkeleton.tsx @@ -0,0 +1,14 @@ +import { Stack, StackProps, Skeleton } from "@material-ui/core"; + +export default function SectionHeadingSkeleton({ sx, ...props }: StackProps) { + return ( + + + + ); +} diff --git a/src/components/Settings/SettingsSection.tsx b/src/components/Settings/SettingsSection.tsx index 751d4a7f..3a105c1c 100644 --- a/src/components/Settings/SettingsSection.tsx +++ b/src/components/Settings/SettingsSection.tsx @@ -1,36 +1,43 @@ -import { Typography, Paper } from "@material-ui/core"; +import { Paper, PaperProps } from "@material-ui/core"; + +import SectionHeading from "components/SectionHeading"; +import SlideTransition from "components/Modal/SlideTransition"; export interface ISettingsSectionProps { children: React.ReactNode; title: string; + paperSx?: PaperProps["sx"]; + transitionTimeout?: number; } export default function SettingsSection({ children, title, + paperSx, + transitionTimeout = 100, }: ISettingsSectionProps) { return (
- - {title} - - + {title} + - "& > :not(style) + :not(style)": { - m: 0, - mt: { xs: 2, sm: 3 }, - }, - }} - > - {children} - + + :not(style) + :not(style)": { + m: 0, + mt: { xs: 2, sm: 3 }, + }, + + ...paperSx, + }} + > + {children} + +
); } diff --git a/src/components/Settings/UserManagement/InviteUser.tsx b/src/components/Settings/UserManagement/InviteUser.tsx new file mode 100644 index 00000000..f7b4d676 --- /dev/null +++ b/src/components/Settings/UserManagement/InviteUser.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; + +import { + Tooltip, + Zoom, + Fab, + DialogContentText, + Link as MuiLink, + TextField, +} from "@material-ui/core"; +import AddIcon from "@material-ui/icons/PersonAddOutlined"; + +import Modal from "components/Modal"; + +import routes from "constants/routes"; + +export default function InviteUser() { + const [open, setOpen] = useState(false); + + return ( + <> + + + setOpen(true)} + color="secondary" + sx={{ + zIndex: "speedDial", + position: "fixed", + bottom: (theme) => ({ + xs: theme.spacing(2), + sm: theme.spacing(3), + }), + right: (theme) => ({ + xs: theme.spacing(2), + sm: theme.spacing(3), + }), + }} + > + + + + + + {open && ( + setOpen(false)} + maxWidth="xs" + body={ + <> + + Send an email to this user to invite them to join your project. + + + They can sign up with any of the sign-in options{" "} + + you have enabled + + , as long as they use the same email address. + + + + } + actions={{ primary: { children: "Invite" } }} + /> + )} + + ); +} diff --git a/src/components/Settings/UserManagement/UserItem.tsx b/src/components/Settings/UserManagement/UserItem.tsx index b3be0631..1c5224dd 100644 --- a/src/components/Settings/UserManagement/UserItem.tsx +++ b/src/components/Settings/UserManagement/UserItem.tsx @@ -26,12 +26,9 @@ export default function UserItem({ *": { userSelect: "all" }, }} /> @@ -55,6 +52,7 @@ export default function UserItem({ }, "& .MuiFilledInput-root": { + bgcolor: "transparent", boxShadow: 0, "&::before": { content: "none" }, @@ -63,7 +61,7 @@ export default function UserItem({ "& .MuiSelect-select.MuiFilledInput-input": { typography: "button", pl: 1, - pr: 3.5, + pr: 3.25, }, "& .MuiSelect-icon": { right: 2, diff --git a/src/components/StyledCard.tsx b/src/components/StyledCard.tsx deleted file mode 100644 index 4bd89a59..00000000 --- a/src/components/StyledCard.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import clsx from "clsx"; -import { Link, LinkProps } from "react-router-dom"; - -import { makeStyles, createStyles } from "@material-ui/styles"; -import { - Card, - Grid, - Typography, - Button, - CardActions, - CardContent, - CardMedia, - Divider, -} from "@material-ui/core"; -import { ButtonProps } from "@material-ui/core/Button"; - -import GoIcon from "assets/icons/Go"; - -const useStyles = makeStyles((theme) => - createStyles({ - root: { width: "100%" }, - container: { height: "100%" }, - cardContent: { "&:last-child": { paddingBottom: 0 } }, - - headerSection: { marginBottom: theme.spacing(1) }, - overline: { - marginBottom: theme.spacing(2), - color: theme.palette.text.secondary, - }, - title: { whiteSpace: "pre-line" }, - image: { - width: 80, - height: 80, - borderRadius: theme.shape.borderRadius, - }, - - cardActions: { - // padding: theme.spacing(1), - - display: "flex", - justifyContent: "space-between", - }, - - divider: { - margin: theme.spacing(2), - marginBottom: 0, - }, - }) -); - -interface StyledCardProps { - className?: string; - - overline?: React.ReactNode; - title?: string; - imageSource?: string; - - bodyContent?: React.ReactNode; - - primaryButton?: Partial; - primaryLink?: { - to: LinkProps["to"]; - children?: React.ReactNode; - label?: string; - }; - secondaryAction?: React.ReactNode; - headerAction?: React.ReactNode; -} - -export default function StyledCard({ - className, - overline, - title, - imageSource, - bodyContent, - primaryButton, - primaryLink, - secondaryAction, - headerAction, -}: StyledCardProps) { - const classes = useStyles(); - - return ( - - - - - - - - - {overline && ( - - {overline} - - )} - - {title && ( - - {title} - - )} - {headerAction && headerAction} - - - - {imageSource && ( - - - - )} - - - - - {bodyContent && Array.isArray(bodyContent) ? ( - - {bodyContent.map((element) => ( - {element} - ))} - - ) : ( - - {bodyContent} - - )} - - - - - - - - - {primaryButton && ( - - - } - sx={{ - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - bgcolor: "background.default", - zIndex: 9999, - }} - /> - ); - } + if (!Array.isArray(tables)) + return view === "list" ? : ; - const TableCard = ({ table }) => { - const checked = Boolean(_find(favs, table)); - return ( - - { - userDoc.dispatch({ - action: DocActions.update, - data: { - favoriteTables: checked - ? favs.filter((t) => t.collection !== table.collection) - : [...favs, table], - }, - }); - }} - checked={checked} - icon={} - checkedIcon={} - name="checkedH" - className={classes.favButton} - /> - } - bodyContent={table.description} - primaryLink={{ - to: `${ - table.isCollectionGroup ? routes.tableGroup : routes.table - }/${table.collection.replace(/\//g, "~2F")}`, - label: "Open", + if (settingsDocState.error?.code === "permission-denied") + return ; + + const createTableFab = ( + + + ({ xs: theme.spacing(2), sm: theme.spacing(3) }), + right: (theme) => ({ xs: theme.spacing(2), sm: theme.spacing(3) }), }} - secondaryAction={ - - setSettingsDialogState({ - mode: TableSettingsDialogModes.update, - data: table, - }) - } - aria-label="Edit table" - > - - - } - /> - + > + + + + + ); + + if (tables.length === 0 && userClaims.roles.includes("ADMIN")) + return ( + <> + + {createTableFab} + ); + + const getLink = (table: Table) => + `${ + table.isCollectionGroup ? routes.tableGroup : routes.table + }/${table.id.replace(/\//g, "~2F")}`; + + const handleFavorite = (id: string) => (e: ChangeEvent) => { + const newFavorites = e.target.checked + ? [...favorites, id] + : favorites.filter((f) => f !== id); + + userDoc.dispatch({ + action: DocActions.update, + data: { favoriteTables: newFavorites }, + }); }; + const getActions = (table: Table) => ( + <> + {userClaims.roles.includes("ADMIN") && ( + + setSettingsDialogState({ + mode: TableSettingsDialogModes.update, + data: table as any, + }) + } + size={view === "list" ? "large" : undefined} + > + + + )} + } + checkedIcon={ + + + + } + name={`favorite-${table.id}`} + inputProps={{ "aria-label": "Favorite" }} + sx={view === "list" ? { p: 1.5 } : undefined} + color="secondary" + /> + + ); + return ( -
- {sections && Object.keys(sections).length > 0 ? ( - - {favs.length !== 0 && ( -
- - Favorites - - - - {favs.map((table) => ( - - ))} - -
- )} + + handleQuery(e.target.value)} + paperSx={{ + maxWidth: (theme) => theme.breakpoints.values.sm - 48, + width: { xs: "100%", md: "50%", lg: "100%" }, + mx: "auto", + mb: { xs: 2, md: -6 }, + }} + /> - {sections && - Object.keys(sections).length > 0 && - Object.keys(sections).map((sectionName) => ( -
- - {sectionName === "undefined" ? "Other" : sectionName} - + + + {query ? `${results.length} of ${tables.length}` : tables.length}{" "} + Tables + - + { + if (v !== null) setView(v); + }} + aria-label="Table view" + sx={{ "& .MuiToggleButton-root": { borderRadius: 2 } }} + > + + + + + + + + - - {sections[sectionName].map((table, i) => ( - - ))} - -
- ))} - -
- - - - - -
-
+ {view === "list" ? ( + ) : ( - - - - - - + )} - -
+ {userClaims.roles.includes("ADMIN") && ( + <> + {createTableFab} + + + )} + ); } diff --git a/src/pages/Settings/ProjectSettings.tsx b/src/pages/Settings/ProjectSettings.tsx index b76d35bd..49bdca2e 100644 --- a/src/pages/Settings/ProjectSettings.tsx +++ b/src/pages/Settings/ProjectSettings.tsx @@ -69,7 +69,7 @@ export default function ProjectSettingsPage() { ); return ( - + {settingsState.loading || publicSettingsState.loading ? ( @@ -78,15 +78,15 @@ export default function ProjectSettingsPage() { ) : ( - + - + - + diff --git a/src/pages/Settings/UserManagement.tsx b/src/pages/Settings/UserManagement.tsx index 6f3805c5..7cf02ccf 100644 --- a/src/pages/Settings/UserManagement.tsx +++ b/src/pages/Settings/UserManagement.tsx @@ -1,12 +1,13 @@ -import { useSearch } from "react-use-search"; - import { Container, Stack, Typography, Paper, List } from "@material-ui/core"; import FloatingSearch from "components/FloatingSearch"; +import SlideTransition from "components/Modal/SlideTransition"; import UserItem from "components/Settings/UserManagement/UserItem"; import UserSkeleton from "components/Settings/UserManagement/UserSkeleton"; +import InviteUser from "components/Settings/UserManagement/InviteUser"; import useCollection from "hooks/useCollection"; +import useBasicSearch from "hooks/useBasicSearch"; import { USERS } from "config/dbPaths"; export interface User { @@ -20,53 +21,66 @@ export interface User { export default function UserManagementPage() { const [usersState] = useCollection({ path: USERS }); + const users: User[] = usersState.documents ?? []; const loading = usersState.loading || !Array.isArray(usersState.documents); - const [filteredUsers, query, handleChange] = useSearch( - usersState.documents ?? [], + const [results, query, handleQuery] = useBasicSearch( + users, (user, query) => - user.id === query || - user.user.displayName.includes(query) || - user.user.email.includes(query), - { filter: true, debounce: 200 } + user.id.toLowerCase() === query || + user.user.displayName.toLowerCase().includes(query) || + user.user.email.toLowerCase().includes(query) ); return ( - - + + handleQuery(e.target.value)} + /> - - - Users - - {!loading && ( - - {query - ? `${filteredUsers.length} of ${usersState.documents.length}` - : usersState.documents.length} + + + + Users - )} - - - - - {loading || (query === "" && filteredUsers.length === 0) ? ( - <> - - - - - ) : ( - filteredUsers.map((user) => ) + {!loading && ( + + {query + ? `${results.length} of ${usersState.documents.length}` + : usersState.documents.length} + )} - - + + + + {loading || (query === "" && results.length === 0) ? ( + + + + + + + + ) : ( + + + + {results.map((user) => ( + + ))} + + + + )} + + ); } diff --git a/src/pages/Settings/UserSettings.tsx b/src/pages/Settings/UserSettings.tsx index eb88fb0a..ce3fc350 100644 --- a/src/pages/Settings/UserSettings.tsx +++ b/src/pages/Settings/UserSettings.tsx @@ -4,7 +4,7 @@ import SettingsSection from "components/Settings/SettingsSection"; export default function UserSettingsPage() { return ( - + TODO: diff --git a/src/theme/colors.ts b/src/theme/colors.ts index 5180d6c8..1543c19f 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -5,18 +5,11 @@ import { colord, extend } from "colord"; import lchPlugin from "colord/plugins/lch"; extend([lchPlugin]); -// declare module "@material-ui/core/styles" { -// interface Palette { -// input: string; -// } -// interface PaletteOptions { -// input?: string; -// } -// } declare module "@material-ui/core/styles/createPalette" { interface TypeAction { activeOpacity: number; input: string; + inputOutline: string; } } @@ -68,6 +61,7 @@ export const colorsLight = ( disabled: textBase.alpha(0.26).toHslString(), disabledBackground: textBase.alpha(0.12).toHslString(), input: "#fff", + inputOutline: shadowBase.alpha(0.12).toRgbString(), }, divider: shadowBase.alpha(0.12).toRgbString(), // Using hsl string breaks table borders }, @@ -159,6 +153,7 @@ export const colorsDark = ( hover: "rgba(255, 255, 255, 0.08)", hoverOpacity: 0.08, input: "rgba(255, 255, 255, 0.06)", + inputOutline: "rgba(255, 255, 255, 0.08)", }, // success: { light: "#34c759" }, }, diff --git a/src/theme/components.tsx b/src/theme/components.tsx index f0355e80..3760787c 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -1,4 +1,5 @@ import { Theme, ThemeOptions } from "@material-ui/core/styles"; +import { toRem } from "./typography"; import RadioIcon from "theme/RadioIcon"; import CheckboxIcon from "theme/CheckboxIcon"; @@ -15,10 +16,6 @@ declare module "@material-ui/core/styles/createTransitions" { } export const components = (theme: Theme): ThemeOptions => { - const colorDividerHalf = colord(theme.palette.divider) - .alpha(colord(theme.palette.divider).alpha() / 2) - .toHslString(); - const buttonPrimaryHover = colord(theme.palette.primary.main) .mix(theme.palette.primary.contrastText, 0.12) .alpha(1) @@ -151,22 +148,29 @@ export const components = (theme: Theme): ThemeOptions => { backgroundColor: theme.palette.action.input, }, - boxShadow: `0 0 0 1px ${ - theme.palette.mode === "dark" - ? colorDividerHalf - : theme.palette.divider - } inset`, - borderRadius: theme.shape.borderRadius, + boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset, + 0 -1px 0 0 ${theme.palette.text.disabled} inset`, + transition: theme.transitions.create("box-shadow", { + duration: theme.transitions.duration.short, + }), - overflow: "hidden", - "&::before": { - borderRadius: theme.shape.borderRadius, - height: (theme.shape.borderRadius as number) * 2, - - borderColor: theme.palette.text.disabled, + "&:hover": { + boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset, + 0 -1px 0 0 ${theme.palette.text.primary} inset`, }, - "&.Mui-focused::before, &.Mui-focused:hover::before": { - borderColor: theme.palette.primary.main, + "&.Mui-focused, &.Mui-focused:hover": { + boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset, + 0 -2px 0 0 ${theme.palette.primary.main} inset`, + }, + + borderRadius: theme.shape.borderRadius, + overflow: "hidden", + "&::before": { content: "none" }, + "&::after": { + width: `calc(100% - ${ + (theme.shape.borderRadius as number) * 2 + }px)`, + left: theme.shape.borderRadius, }, "&.Mui-disabled": { @@ -181,9 +185,14 @@ export const components = (theme: Theme): ThemeOptions => { borderColor: theme.palette.secondary.main, }, }, + input: { + paddingTop: theme.spacing(1.5), + paddingBottom: theme.spacing(13 / 8), + height: toRem(23), + }, inputSizeSmall: { - padding: "6px 12px", - height: 20, + padding: theme.spacing(0.75, 1.5), + height: toRem(20), }, multiline: { padding: 0 }, }, @@ -272,7 +281,7 @@ export const components = (theme: Theme): ThemeOptions => { "& .MuiListItemIcon-root": { minWidth: 24 + 12, - "& svg": { fontSize: "1.5rem" }, + "& svg": { fontSize: toRem(24) }, }, "& + .MuiDivider-root": { @@ -344,12 +353,10 @@ export const components = (theme: Theme): ThemeOptions => { outlined: { "&, &:hover, &.Mui-disabled": { border: "none" }, - boxShadow: - theme.palette.mode === "dark" - ? `0 0 0 1px ${colorDividerHalf} inset, - 0 1px 0 0 ${colorDividerHalf} inset` - : `0 0 0 1px ${theme.palette.divider} inset, - 0 -1px 0 0 ${theme.palette.divider} inset`, + boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset, + 0 ${theme.palette.mode === "dark" ? "" : "-"}1px 0 0 ${ + theme.palette.action.inputOutline + } inset`, backgroundColor: theme.palette.action.input, "&.Mui-disabled": { @@ -406,7 +413,10 @@ export const components = (theme: Theme): ThemeOptions => { TouchRippleProps: { center: false }, }, styleOverrides: { - sizeSmall: { borderRadius: theme.shape.borderRadius }, + sizeSmall: { + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(0.5), + }, }, }, MuiFab: { @@ -596,6 +606,9 @@ export const components = (theme: Theme): ThemeOptions => { icon: , checkedIcon: , }, + styleOverrides: { + root: { padding: theme.spacing(1) }, + }, }, MuiCheckbox: { defaultProps: { @@ -603,6 +616,9 @@ export const components = (theme: Theme): ThemeOptions => { checkedIcon: , indeterminateIcon: , }, + styleOverrides: { + root: { padding: theme.spacing(1) }, + }, }, MuiSlider: { diff --git a/yarn.lock b/yarn.lock index b1a26128..c27fe9a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3201,7 +3201,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@^5.1.7": +"@types/react-router-dom@*", "@types/react-router-dom@^5.1.7": version "5.1.8" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.8.tgz#bf3e1c8149b3d62eaa206d58599de82df0241192" integrity sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw== @@ -3210,6 +3210,14 @@ "@types/react" "*" "@types/react-router" "*" +"@types/react-router-hash-link@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/react-router-hash-link/-/react-router-hash-link-2.4.1.tgz#9e5da328817e12f489a2005613857cbf50127e61" + integrity sha512-SPVymyscQUBWMAPEpAn3I35MQXarTx0rOPmcfHl1xWYaTSDP5kxQnrFjmMxJlX5mjIPVHb3XBm8t6DTQNjkGEQ== + dependencies: + "@types/react" "*" + "@types/react-router-dom" "*" + "@types/react-router@*": version "5.1.16" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.16.tgz#f3ba045fb96634e38b21531c482f9aeb37608a99" @@ -13634,6 +13642,13 @@ react-router-dom@^5.0.1: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-router-hash-link@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz#570824d53d6c35ce94d73a46c8e98673a127bf08" + integrity sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A== + dependencies: + prop-types "^15.7.2" + react-router@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" @@ -13740,13 +13755,6 @@ react-transition-group@^4.4.0, react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" -react-use-search@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/react-use-search/-/react-use-search-0.3.1.tgz#1a36434a0f0a0fc2600a743a2c170ad88f718787" - integrity sha512-Ec8+5AnOMunnA532ziNLzs3W/4l9MhecvRF1fi2ho9JvXBgT/pmdFpgN3fRfPCHAsMf9LpEo4LxDZDs/BdTfNQ== - dependencies: - lodash.debounce "^4.0.8" - react-usestateref@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.5.tgz#0b5659217022a7e88087875ec1745730940b7882"