replace desktop pinned nav with collapsible nav

This commit is contained in:
Sidney Alcantara
2022-07-04 19:13:30 +10:00
parent 13d37efc90
commit 4f230871e1
20 changed files with 561 additions and 412 deletions

View File

@@ -13,8 +13,6 @@ export const altPressAtom = atom(false);
/** Nav open state stored in local storage. */
export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false);
/** Nav pinned state stored in local storage. */
export const navPinnedAtom = atomWithStorage("__ROWY__NAV_PINNED", false);
/** View for tables page */
export const tablesViewAtom = atomWithStorage<"grid" | "list">(

View File

@@ -8,7 +8,7 @@ import {
import SearchIcon from "@mui/icons-material/Search";
import SlideTransition from "@src/components/Modal/SlideTransition";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
export interface IFloatingSearchProps extends Partial<FilledTextFieldProps> {
label: string;
@@ -26,7 +26,7 @@ export default function FloatingSearch({
});
const docked = useScrollTrigger({
disableHysteresis: true,
threshold: APP_BAR_HEIGHT,
threshold: TOP_BAR_HEIGHT,
});
return (

View File

@@ -5,7 +5,7 @@ import { HashLink } from "react-router-hash-link";
import { Stack, StackProps, Typography, IconButton } from "@mui/material";
import LinkIcon from "@mui/icons-material/Link";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
export interface ISectionHeadingProps extends Omit<StackProps, "children"> {
children: string;
@@ -35,7 +35,7 @@ export const SectionHeading = forwardRef(function SectionHeading_(
opacity: 1,
},
scrollMarginTop: (theme) => theme.spacing(APP_BAR_HEIGHT / 8 + 3.5),
scrollMarginTop: (theme) => theme.spacing(TOP_BAR_HEIGHT / 8 + 3.5),
scrollBehavior: "smooth",
}}
>

View File

@@ -1,6 +1,6 @@
import { styled, Drawer, drawerClasses } from "@mui/material";
import { DRAWER_WIDTH, DRAWER_COLLAPSED_WIDTH } from "./index";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
export const StyledDrawer = styled(Drawer)(({ theme }) => ({
@@ -32,15 +32,15 @@ export const StyledDrawer = styled(Drawer)(({ theme }) => ({
boxSizing: "content-box",
top: APP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT,
height: `calc(100% - ${APP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT}px)`,
top: TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT,
height: `calc(100% - ${TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT}px)`,
".MuiDialog-paperFullScreen &": {
top:
APP_BAR_HEIGHT +
TOP_BAR_HEIGHT +
TABLE_TOOLBAR_HEIGHT +
Number(theme.spacing(2).replace("px", "")),
height: `calc(100% - ${
APP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT
TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT
}px - ${theme.spacing(2)})`,
},

View File

@@ -13,7 +13,7 @@ import {
columnModalAtom,
tableModalAtom,
} from "@src/atoms/tableScope";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
export default function EmptyTable() {
const openColumnModal = useSetAtom(columnModalAtom, tableScope);
@@ -130,7 +130,7 @@ export default function EmptyTable() {
justifyContent="center"
alignItems="center"
sx={{
height: `calc(100vh - ${APP_BAR_HEIGHT}px)`,
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
width: "100%",
p: 2,
maxWidth: 480,

View File

@@ -29,7 +29,6 @@ import {
globalScope,
userRolesAtom,
userSettingsAtom,
navPinnedAtom,
} from "@src/atoms/globalScope";
import {
tableScope,
@@ -66,7 +65,6 @@ export default function Table({
}) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [userSettings] = useAtom(userSettingsAtom, globalScope);
const [navPinned] = useAtom(navPinnedAtom, globalScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
@@ -188,8 +186,9 @@ export default function Table({
const target = event.target as HTMLDivElement;
if (navPinned && !columns[0].fixed)
setShowLeftScrollDivider(target.scrollLeft > 16);
// TODO:
// if (navPinned && !columns[0].fixed)
// setShowLeftScrollDivider(target.scrollLeft > 16);
const offset = 800;
const isAtBottom =

View File

@@ -1,6 +1,6 @@
import { colord } from "colord";
import { styled, alpha, darken, lighten } from "@mui/material";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
import {
DRAWER_COLLAPSED_WIDTH,
@@ -15,7 +15,7 @@ export const TableContainer = styled("div", {
display: "flex",
position: "relative",
flexDirection: "column",
height: `calc(100vh - ${APP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px)`,
height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px)`,
"& .left-scroll-divider": {
position: "absolute",

View File

@@ -27,6 +27,7 @@ const WIKI_PATHS = {
setupRoles: "/setup/roles",
setupUpdate: "/setup/update",
howTo: "/category/quickstart-guide",
howToCreateTable: "/how-to/create-table",
howToCreateColumn: "/how-to/create-column",
howToAddRow: "/how-to/add-row",

View File

@@ -1,6 +1,6 @@
import Logo from "@src/assets/Logo";
import BreadcrumbsTableRoot from "@src/components/Table/BreadcrumbsTableRoot";
import { GrowProps } from "@mui/material";
import { FadeProps } from "@mui/material";
export enum ROUTES {
home = "/",
@@ -46,22 +46,15 @@ export enum ROUTES {
export const ROUTE_TITLES = {
[ROUTES.tables]: {
title: "Tables",
titleComponent: (open, pinned) =>
!(open && pinned) && (
<Logo
style={{
display: "block",
margin: "0 auto",
}}
/>
titleComponent: (open, isPermanent) =>
!(open && isPermanent) && (
<Logo style={{ display: "block", margin: "0 auto" }} />
),
},
[ROUTES.table]: {
title: "Table",
titleComponent: (open, pinned) => (
<BreadcrumbsTableRoot sx={{ ml: open && pinned ? -48 / 8 : 2 }} />
),
titleComponent: (_open, _isPermanent) => <BreadcrumbsTableRoot />,
titleTransitionProps: { style: { transformOrigin: "0 50%" } },
leftAligned: true,
},
@@ -80,8 +73,8 @@ export const ROUTE_TITLES = {
| string
| {
title: string;
titleComponent: (open: boolean, pinned: boolean) => React.ReactNode;
titleTransitionProps?: Partial<GrowProps>;
titleComponent: (open: boolean, isPermanent: boolean) => React.ReactNode;
titleTransitionProps?: Partial<FadeProps>;
leftAligned?: boolean;
}
>;

View File

@@ -11,7 +11,7 @@ import {
Grow,
} from "@mui/material";
import HelpIcon from "@mui/icons-material/HelpOutline";
import DocsIcon from "@mui/icons-material/DescriptionOutlined";
import DocsIcon from "@mui/icons-material/LibraryBooksOutlined";
import { Discord as DiscordIcon } from "@src/assets/icons";
import GitHubIcon from "@mui/icons-material/GitHub";
import TwitterIcon from "@mui/icons-material/Twitter";
@@ -44,7 +44,6 @@ export default function UserMenu(props: IconButtonProps) {
setOpen(true);
analytics.logEvent("open_help_menu");
}}
sx={{ ml: 1.5 }}
>
<HelpIcon />
</IconButton>

View File

@@ -1,5 +1,7 @@
import { useState } from "react";
import { useAtom, useSetAtom } from "jotai";
import { find, groupBy } from "lodash-es";
import { colord } from "colord";
import {
Drawer,
@@ -9,19 +11,25 @@ import {
List,
ListItemIcon,
ListItemText,
// ListSubheader,
Avatar,
Divider,
ListItemSecondaryAction,
Box,
Fade,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/MenuOpen";
import PinIcon from "@mui/icons-material/PushPinOutlined";
import UnpinIcon from "@mui/icons-material/PushPin";
import ArrowBackIcon from "@mui/icons-material/WorkspacesOutlined";
import HomeIcon from "@mui/icons-material/HomeOutlined";
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
import ProjectSettingsIcon from "@mui/icons-material/BuildCircleOutlined";
import UserManagementIcon from "@mui/icons-material/AccountCircleOutlined";
import AddIcon from "@mui/icons-material/Add";
import { Table as TableIcon } from "@src/assets/icons";
import DocsIcon from "@mui/icons-material/LibraryBooksOutlined";
import LearningIcon from "@mui/icons-material/LocalLibraryOutlined";
import HelpIcon from "@mui/icons-material/HelpOutline";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { APP_BAR_HEIGHT } from ".";
import Logo from "@src/assets/Logo";
import NavItem from "./NavItem";
import NavTableSection from "./NavTableSection";
@@ -36,24 +44,22 @@ import {
} from "@src/atoms/globalScope";
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
import { TOP_BAR_HEIGHT } from "./TopBar";
export const NAV_DRAWER_WIDTH = 256;
export const NAV_DRAWER_COLLAPSED_WIDTH = 56;
export interface INavDrawerProps extends DrawerProps {
open: boolean;
isPermanent: boolean;
onClose: NonNullable<DrawerProps["onClose"]>;
pinned: boolean;
setPinned: React.Dispatch<React.SetStateAction<boolean>>;
canPin: boolean;
scrollTrigger: boolean;
}
export default function NavDrawer({
open,
pinned,
setPinned,
canPin,
scrollTrigger,
...props
isPermanent,
onClose,
}: INavDrawerProps) {
const [tables] = useAtom(tablesAtom, globalScope);
const [userSettings] = useAtom(userSettingsAtom, globalScope);
@@ -63,6 +69,8 @@ export default function NavDrawer({
globalScope
);
const [hover, setHover] = useState(false);
const favorites = Array.isArray(userSettings.favoriteTables)
? userSettings.favoriteTables
: [];
@@ -73,146 +81,145 @@ export default function NavDrawer({
...groupBy(tables, "section"),
};
const closeDrawer = pinned
const collapsed = !open && isPermanent;
const tempExpanded = hover && collapsed;
const width =
collapsed && !tempExpanded ? NAV_DRAWER_COLLAPSED_WIDTH : NAV_DRAWER_WIDTH;
const closeDrawer = isPermanent
? undefined
: (e: {}) => props.onClose(e, "escapeKeyDown");
: (e: {}) => onClose(e, "escapeKeyDown");
const externalLinkIcon = !collapsed && (
<ListItemSecondaryAction sx={{ right: 10, color: "text.disabled" }}>
<InlineOpenInNewIcon />
</ListItemSecondaryAction>
);
return (
<Drawer
open={open}
{...props}
variant={pinned ? "persistent" : "temporary"}
anchor="left"
sx={{
width: open ? NAV_DRAWER_WIDTH : 0,
transition: (theme) =>
theme.transitions.create("width", {
easing: pinned
? theme.transitions.easing.easeOut
: theme.transitions.easing.sharp,
duration: pinned
? theme.transitions.duration.enteringScreen
: theme.transitions.duration.leavingScreen,
}),
flexShrink: 0,
"& .MuiDrawer-paper": {
minWidth: NAV_DRAWER_WIDTH,
borderRight: "none",
bgcolor: pinned ? "background.default" : "background.paper",
},
}}
>
<Stack
direction="row"
alignItems="center"
sx={{
height: APP_BAR_HEIGHT,
flexShrink: 0,
px: 0.5,
position: "sticky",
top: 0,
zIndex: "appBar",
backgroundColor:
pinned && scrollTrigger ? "background.paper" : "inherit",
backgroundImage: pinned
? "linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))" // Elevation 8
: "inherit",
"&::before": {
content: "''",
display: "block",
position: "absolute",
inset: 0,
bgcolor: "background.default",
opacity: pinned ? (scrollTrigger ? 0 : 1) : 0,
transition: (theme) =>
theme.transitions.create("opacity", {
easing:
canPin && pinned
? theme.transitions.easing.easeOut
: theme.transitions.easing.sharp,
duration:
canPin && pinned
? theme.transitions.duration.enteringScreen
: theme.transitions.duration.leavingScreen,
}),
<>
<Drawer
open={isPermanent || open}
onClose={onClose}
hideBackdrop={isPermanent}
ModalProps={{ disablePortal: true }}
variant={isPermanent ? "permanent" : "temporary"}
anchor="left"
sx={[
{
width,
flexShrink: 0,
"& .MuiDrawer-paper": {
width,
pt: 0,
pb: 1,
},
},
boxShadow: pinned && scrollTrigger ? 1 : 0,
transition: (theme) =>
theme.transitions.create(["background-color", "box-shadow"], {
easing:
canPin && pinned
? theme.transitions.easing.easeOut
: theme.transitions.easing.sharp,
duration:
canPin && pinned
? theme.transitions.duration.enteringScreen
: theme.transitions.duration.leavingScreen,
}),
}}
isPermanent && {
position: "fixed",
zIndex: (theme) => theme.zIndex.appBar - 1,
"& .MuiDrawer-paper": {
mt: `${TOP_BAR_HEIGHT - 4}px`,
height: `calc(100% - ${TOP_BAR_HEIGHT - 4}px)`,
pt: 0.5,
borderRadius: 2,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
width,
transitionProperty:
"width, transform, background-color, box-shadow",
transitionTimingFunction: "var(--nav-transition-timing-function)",
transitionDuration: "var(--nav-transition-duration)",
overflowX: "hidden",
borderRight: "none",
bgcolor: "background.default",
},
"& .MuiListItemSecondaryAction-root": {
transitionProperty: "opacity",
transitionTimingFunction: "var(--nav-transition-timing-function)",
transitionDuration: "var(--nav-transition-duration)",
},
},
collapsed &&
!tempExpanded && {
"& .MuiMenuItem-root": {},
"& .MuiListItemSecondaryAction-root": {
opacity: 0,
transitionDelay: "0ms",
},
},
tempExpanded && {
zIndex: "drawer",
"& .MuiDrawer-paper": {
bgcolor: (theme) =>
colord(theme.palette.background.paper)
.mix("#fff", 0.09)
.alpha(1)
.toHslString(),
boxShadow: (theme) =>
theme.shadows[4].replace(/, 0 (\d+px)/g, ", $1 0"),
},
},
]}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<IconButton
aria-label="Close navigation drawer"
onClick={props.onClose as any}
size="large"
>
<CloseIcon />
</IconButton>
<Logo style={{ marginLeft: 1, position: "relative", zIndex: 1 }} />
{canPin && (
<IconButton
aria-label="Pin navigation drawer"
onClick={() => setPinned((p) => !p)}
aria-pressed={pinned}
size="large"
style={{ marginLeft: "auto" }}
{!isPermanent && (
<Stack
direction="row"
alignItems="center"
sx={{ height: TOP_BAR_HEIGHT, flexShrink: 0, px: 0.5 }}
>
{pinned ? <UnpinIcon /> : <PinIcon />}
</IconButton>
<IconButton
aria-label="Close navigation drawer"
onClick={onClose as any}
size="large"
>
<CloseIcon />
</IconButton>
<Logo style={{ marginLeft: 1, position: "relative", zIndex: 1 }} />
</Stack>
)}
</Stack>
<nav>
<List>
<li>
<NavItem to={ROUTES.tables} onClick={closeDrawer}>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Home" />
</NavItem>
</li>
{userRoles.includes("ADMIN") && (
<Divider variant="middle" sx={{ my: 1 }} />
)}
<li>
<NavItem to={ROUTES.userSettings} onClick={closeDrawer}>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</NavItem>
</li>
{userRoles.includes("ADMIN") && (
<nav style={{ flexGrow: 1 }}>
<List
disablePadding
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<li>
<NavItem to={ROUTES.projectSettings} onClick={closeDrawer}>
<NavItem onClick={closeDrawer}>
<ListItemIcon>
<ProjectSettingsIcon />
<ArrowBackIcon />
</ListItemIcon>
<ListItemText primary="Project Settings" />
<UpdateCheckBadge sx={{ mr: 1.5 }} />
<ListItemText primary="Workspace" />
</NavItem>
</li>
)}
{userRoles.includes("ADMIN") && (
<li>
<NavItem onClick={closeDrawer}>
<ListItemIcon>
<Avatar
sx={{
borderRadius: 1,
width: 24,
height: 24,
fontSize: "inherit",
bgcolor: "primary.main",
color: "primary.contrastText",
}}
>
P
</Avatar>
</ListItemIcon>
<ListItemText primary="Project" />
</NavItem>
</li>
{/* {userRoles.includes("ADMIN") && (
<li>
<NavItem to={ROUTES.userManagement} onClick={closeDrawer}>
<ListItemIcon>
@@ -221,42 +228,126 @@ export default function NavDrawer({
<ListItemText primary="User Management" />
</NavItem>
</li>
)}
)} */}
<Divider variant="middle" sx={{ my: 1 }} />
<Divider variant="middle" sx={{ my: 1 }} />
{/* <ListSubheader>Your tables</ListSubheader> */}
<li>
<NavItem to={ROUTES.tables} onClick={closeDrawer}>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Home" />
</NavItem>
</li>
<li>
<NavItem
{...({ component: "button" } as any)}
style={{ textAlign: "left" }}
sx={{ mb: 1 }}
onClick={(e) => {
if (closeDrawer) closeDrawer(e);
openTableSettingsDialog({});
}}
>
<li>
<NavItem to={ROUTES.userSettings} onClick={closeDrawer}>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</NavItem>
</li>
{userRoles.includes("ADMIN") && (
<li>
<NavItem to={ROUTES.projectSettings} onClick={closeDrawer}>
<ListItemIcon>
<ProjectSettingsIcon />
</ListItemIcon>
<ListItemText primary="Project Settings" />
<UpdateCheckBadge sx={{ mr: 1.5 }} />
</NavItem>
</li>
)}
<Divider variant="middle" sx={{ my: 1 }} />
{/* <ListSubheader>Your tables</ListSubheader> */}
<li>
<NavItem
{...({ component: "button" } as any)}
style={{ textAlign: "left" }}
onClick={(e) => {
if (closeDrawer) closeDrawer(e);
openTableSettingsDialog({});
}}
>
<ListItemIcon>
<AddIcon />
</ListItemIcon>
<ListItemText primary="Create table…" />
</NavItem>
</li>
{/* <li>
<NavItem to={ROUTES.tables} onClick={closeDrawer}>
<ListItemIcon>
<AddIcon />
<TableIcon />
</ListItemIcon>
<ListItemText primary="Create table…" />
<ListItemText primary="Table" />
</NavItem>
</li>
</li> */}
{sections &&
Object.entries(sections)
.filter(([, tables]) => tables.length > 0)
.map(([section, tables]) => (
<NavTableSection
key={section}
section={section}
tables={tables}
closeDrawer={closeDrawer}
/>
))}
</List>
</nav>
</Drawer>
{sections &&
Object.entries(sections)
.filter(([, tables]) => tables.length > 0)
.map(([section, tables]) => (
<NavTableSection
key={section}
section={section}
tables={tables}
closeDrawer={closeDrawer}
hidden={isPermanent && !open && !tempExpanded}
/>
))}
<Divider variant="middle" sx={{ my: 1, mt: "auto" }} />
<li>
<NavItem href={EXTERNAL_LINKS.docs}>
<ListItemIcon>
<DocsIcon />
</ListItemIcon>
<ListItemText primary="Docs" />
{externalLinkIcon}
</NavItem>
</li>
<li>
<NavItem href={WIKI_LINKS.howTo}>
<ListItemIcon>
<LearningIcon />
</ListItemIcon>
<ListItemText primary="Learning" />
{externalLinkIcon}
</NavItem>
</li>
<li>
<NavItem href={EXTERNAL_LINKS.docs}>
<ListItemIcon>
<HelpIcon />
</ListItemIcon>
<ListItemText primary="Help" />
</NavItem>
</li>
</List>
</nav>
</Drawer>
{isPermanent && (
<Box
sx={{
flexShrink: 0,
flexGrow: 0,
width: open ? NAV_DRAWER_WIDTH : NAV_DRAWER_COLLAPSED_WIDTH,
transitionProperty: "width",
transitionTimingFunction: "var(--nav-transition-timing-function)",
transitionDuration: "var(--nav-transition-duration)",
}}
/>
)}
</>
);
}

View File

@@ -1,29 +1,35 @@
import { Link, useLocation } from "react-router-dom";
import { MenuItem, MenuItemProps } from "@mui/material";
import { spreadSx } from "@src/utils/ui";
export default function NavItem(props: MenuItemProps<typeof Link>) {
const linkProps = { target: "_blank", rel: "noopener noreferrer" };
export default function NavItem(props: MenuItemProps<typeof Link | "a">) {
const { pathname } = useLocation();
return (
<MenuItem
component={Link}
selected={pathname === props.to}
component={"to" in props ? Link : "a"}
selected={"to" in props ? pathname === props.to : false}
{...props}
sx={{
"& .MuiListItemText-primary": {
typography: "button",
color: "text.secondary",
},
"& .MuiListItemIcon-root": { opacity: 0.87 },
{...("href" in props ? linkProps : {})}
sx={[
{
overflow: "hidden",
"&:hover, &.Mui-selected": {
"& .MuiListItemText-primary": { color: "text.primary" },
"& .MuiSvgIcon-root": { color: "text.primary" },
},
"& .MuiListItemText-primary": {
typography: "button",
color: "text.secondary",
},
"& .MuiListItemIcon-root": { opacity: 0.87 },
...props.sx,
"&&::before": { left: "auto", right: 0 },
}}
"&:hover, &.Mui-selected": {
"& .MuiListItemText-primary": { color: "text.primary" },
"& .MuiSvgIcon-root": { color: "text.primary" },
},
},
...spreadSx(props.sx),
]}
/>
);
}

View File

@@ -1,7 +1,13 @@
import { useState } from "react";
import { useLocation } from "react-router-dom";
import { List, ListItemIcon, ListItemText, Collapse } from "@mui/material";
import {
List,
ListItemIcon,
ListItemText,
ListItemSecondaryAction,
Collapse,
} from "@mui/material";
import FolderIcon from "@mui/icons-material/FolderOutlined";
import FavoriteIcon from "@mui/icons-material/FavoriteBorder";
import { ChevronDown } from "@src/assets/icons";
@@ -17,12 +23,14 @@ export interface INavTableSectionProps {
section: string;
tables: TableSettings[];
closeDrawer?: (e: {}) => void;
hidden?: boolean;
}
export default function NavTableSection({
section,
tables,
closeDrawer,
hidden,
}: INavTableSectionProps) {
const { pathname } = useLocation();
const hasMatch = tables.map(getTableRoute).includes(pathname);
@@ -31,7 +39,14 @@ export default function NavTableSection({
const isFavorites = section === "Favorites";
return (
<li>
<li
style={{
opacity: hidden ? 0.38 : 1,
transitionProperty: "opacity",
transitionTimingFunction: "var(--nav-transition-timing-function)",
transitionDuration: "var(--nav-transition-duration)",
}}
>
<NavItem
{...({ component: "button" } as any)}
selected={!isFavorites && hasMatch && !open}
@@ -43,14 +58,17 @@ export default function NavTableSection({
<ListItemText primary={section} style={{ textAlign: "left" }} />
<ChevronDown
sx={{
color: "action.active",
mr: -0.5,
transform: open ? "rotate(180deg)" : "rotate(0)",
transition: (theme) => theme.transitions.create("transform"),
}}
/>
<ListItemSecondaryAction>
<ChevronDown
sx={{
color: "action.active",
m: 0.25,
display: "block",
transform: open ? "rotate(180deg)" : "rotate(0)",
transition: (theme) => theme.transitions.create("transform"),
}}
/>
</ListItemSecondaryAction>
</NavItem>
<Collapse in={open}>

View File

@@ -3,51 +3,28 @@ import { useAtom } from "jotai";
import { ErrorBoundary } from "react-error-boundary";
import { useLocation, Outlet } from "react-router-dom";
import {
useScrollTrigger,
useMediaQuery,
Stack,
AppBar,
Toolbar,
IconButton,
Box,
Typography,
Grow,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import { useMediaQuery, Stack, GlobalStyles } from "@mui/material";
import NavDrawer, { NAV_DRAWER_WIDTH } from "./NavDrawer";
import HelpMenu from "./HelpMenu";
import UserMenu from "./UserMenu";
import TopBar, { TOP_BAR_HEIGHT } from "./TopBar";
import NavDrawer from "./NavDrawer";
import ErrorFallback, {
IErrorFallbackProps,
} from "@src/components/ErrorFallback";
import Loading from "@src/components/Loading";
import UpdateCheckBadge from "./UpdateCheckBadge";
import {
globalScope,
projectIdAtom,
userRolesAtom,
navOpenAtom,
navPinnedAtom,
} from "@src/atoms/globalScope";
import { ROUTE_TITLES } from "@src/constants/routes";
import { useDocumentTitle } from "@src/hooks/useDocumentTitle";
export const APP_BAR_HEIGHT = 56;
const StyledErrorFallback = (props: IErrorFallbackProps) => (
<ErrorFallback {...props} style={{ marginTop: -APP_BAR_HEIGHT }} />
);
export default function Navigation({ children }: React.PropsWithChildren<{}>) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [open, setOpen] = useAtom(navOpenAtom, globalScope);
const [pinned, setPinned] = useAtom(navPinnedAtom, globalScope);
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
const canPin = !useMediaQuery((theme: any) => theme.breakpoints.down("lg"));
const isPermanent = useMediaQuery((theme: any) => theme.breakpoints.up("md"));
const { pathname } = useLocation();
const basePath = ("/" + pathname.split("/")[1]) as keyof typeof ROUTE_TITLES;
@@ -60,165 +37,52 @@ export default function Navigation({ children }: React.PropsWithChildren<{}>) {
return (
<>
<AppBar
position="sticky"
color="inherit"
elevation={trigger ? 1 : 0}
sx={{
height: APP_BAR_HEIGHT,
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))", // Elevation 8
"&::before": {
content: "''",
display: "block",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
bgcolor: "background.default",
opacity: trigger ? 0 : 1,
transition: (theme) =>
theme.transitions.create("opacity", {
easing:
canPin && pinned
? theme.transitions.easing.easeOut
: theme.transitions.easing.sharp,
duration:
canPin && pinned
? theme.transitions.duration.enteringScreen
: theme.transitions.duration.leavingScreen,
}),
},
ml: canPin && pinned && open ? `${NAV_DRAWER_WIDTH}px` : 0,
width:
canPin && pinned && open
? `calc(100% - ${NAV_DRAWER_WIDTH}px)`
: "100%",
transition: (theme) =>
theme.transitions.create(["margin-left", "width", "box-shadow"], {
easing:
canPin && pinned
? theme.transitions.easing.easeOut
: theme.transitions.easing.sharp,
duration:
canPin && pinned
? theme.transitions.duration.enteringScreen
: theme.transitions.duration.leavingScreen,
}),
}}
>
<Toolbar
sx={{
height: APP_BAR_HEIGHT,
minWidth: 0,
maxWidth: "none",
"&&": {
minHeight: APP_BAR_HEIGHT,
p: 0,
pl: (theme) =>
`max(env(safe-area-inset-left), ${theme.spacing(2)})`,
pr: (theme) =>
`max(env(safe-area-inset-right), ${theme.spacing(2)})`,
},
}}
>
{!(open && canPin && pinned) && (
<Grow in>
<IconButton
aria-label="Open navigation drawer"
onClick={() => setOpen(true)}
size="large"
edge="start"
>
{userRoles.includes("ADMIN") ? (
<UpdateCheckBadge>
<MenuIcon />
</UpdateCheckBadge>
) : (
<MenuIcon />
)}
</IconButton>
</Grow>
)}
<Grow
in
key={title}
{...(typeof routeTitle !== "string"
? routeTitle.titleTransitionProps
: undefined)}
>
<Box
sx={{
flex: 1,
overflowX: "auto",
userSelect: "none",
pl: open && canPin && pinned ? 48 / 8 : 0,
pr: 1,
ml: (routeTitle as any)?.leftAligned
? 0
: { xs: 0, sm: 1.5 + 6 + 1 },
}}
>
{typeof routeTitle !== "string" ? (
routeTitle.titleComponent(open, canPin && pinned)
) : (
<Typography
variant="h6"
component="h1"
textAlign="center"
noWrap
sx={{
typography: { sm: "h5" },
textAlign: { xs: "left", sm: "center" },
}}
>
{title}
</Typography>
)}
</Box>
</Grow>
<HelpMenu />
<UserMenu />
</Toolbar>
</AppBar>
<TopBar
open={open}
setOpen={setOpen}
isPermanent={isPermanent}
routeTitle={routeTitle}
title={title}
/>
<Stack direction="row">
<NavDrawer
open={open}
pinned={canPin && pinned}
setPinned={setPinned}
canPin={canPin}
isPermanent={isPermanent}
onClose={() => setOpen(false)}
scrollTrigger={trigger}
/>
<ErrorBoundary FallbackComponent={StyledErrorFallback}>
<Suspense
fallback={
<Loading fullScreen style={{ marginTop: -APP_BAR_HEIGHT }} />
<Loading fullScreen style={{ marginTop: -TOP_BAR_HEIGHT }} />
}
>
<div
style={{
flexGrow: 1,
maxWidth:
canPin && pinned && open
? `calc(100% - ${NAV_DRAWER_WIDTH}px)`
: "100%",
}}
>
<div style={{ flexGrow: 1, maxWidth: "100%" }}>
<Outlet />
{children}
</div>
</Suspense>
</ErrorBoundary>
</Stack>
<GlobalStyles
styles={(theme) => ({
":root": {
"--nav-transition-timing-function": open
? theme.transitions.easing.easeOut
: theme.transitions.easing.sharp,
"--nav-transition-duration":
(open
? theme.transitions.duration.enteringScreen
: theme.transitions.duration.leavingScreen) + "ms",
},
})}
/>
</>
);
}
const StyledErrorFallback = (props: IErrorFallbackProps) => (
<ErrorFallback {...props} style={{ marginTop: -TOP_BAR_HEIGHT }} />
);

View File

@@ -0,0 +1,177 @@
import { useAtom } from "jotai";
import {
useScrollTrigger,
AppBar,
Toolbar,
IconButton,
Box,
Typography,
Grow,
Fade,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import MenuCloseIcon from "@mui/icons-material/MenuOpen";
import Logo from "@src/assets/Logo";
import { NAV_DRAWER_WIDTH, NAV_DRAWER_COLLAPSED_WIDTH } from "./NavDrawer";
import HelpMenu from "./HelpMenu";
import UserMenu from "./UserMenu";
import UpdateCheckBadge from "./UpdateCheckBadge";
import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
import { ROUTE_TITLES } from "@src/constants/routes";
export const TOP_BAR_HEIGHT = 56;
export interface ITopBarProps {
open: boolean;
setOpen: (open: boolean) => void;
isPermanent: boolean;
routeTitle: typeof ROUTE_TITLES[keyof typeof ROUTE_TITLES];
title: string;
}
export default function TopBar({
open,
setOpen,
isPermanent,
routeTitle,
title,
}: ITopBarProps) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
const menuIcon = open ? <MenuCloseIcon /> : <MenuIcon />;
return (
<AppBar
position="sticky"
color="inherit"
elevation={trigger ? 1 : 0}
sx={{
height: TOP_BAR_HEIGHT,
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))", // Elevation 8
"&::before": {
content: "''",
display: "block",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
bgcolor: "background.default",
opacity: trigger ? 0 : 1,
transitionProperty: "opacity",
transitionTimingFunction: "var(--nav-transition-timing-function)",
transitionDuration: "var(--nav-transition-duration)",
},
}}
>
<Toolbar
sx={{
height: TOP_BAR_HEIGHT,
minWidth: 0,
maxWidth: "none",
"&&": {
minHeight: TOP_BAR_HEIGHT,
p: 0,
pl: (theme) =>
`max(env(safe-area-inset-left), ${theme.spacing(0.5)})`,
pr: (theme) =>
`max(env(safe-area-inset-right), ${theme.spacing(0.5)})`,
},
}}
>
<Grow in>
<IconButton
aria-label={`${open ? "Close" : "Open"} navigation drawer`}
onClick={() => setOpen(!open)}
size="large"
>
{userRoles.includes("ADMIN") ? (
<UpdateCheckBadge>{menuIcon}</UpdateCheckBadge>
) : (
menuIcon
)}
</IconButton>
</Grow>
{isPermanent && (
<Box
sx={{
width: open ? NAV_DRAWER_WIDTH : NAV_DRAWER_COLLAPSED_WIDTH,
height: 2,
transform: `scale(${open ? 1 : 0})`,
transformOrigin: "0% 50%",
opacity: open ? 1 : 0,
transitionProperty: "width, opacity, transform",
transitionTimingFunction: "var(--nav-transition-timing-function)",
transitionDuration: "var(--nav-transition-duration)",
display: "flex",
justifyContent: "flex-start",
alignItems: "center",
pointerEvents: "none",
}}
>
<Logo />
</Box>
)}
<Fade
in
key={title}
{...(typeof routeTitle !== "string"
? routeTitle.titleTransitionProps
: undefined)}
>
<Box
sx={[
{
flex: 1,
overflowX: "auto",
userSelect: "none",
ml: 1,
},
!(routeTitle as any)?.leftAligned && {
ml: { xs: 1, sm: 6 },
},
isPermanent &&
(routeTitle as any)?.leftAligned && {
ml: -NAV_DRAWER_COLLAPSED_WIDTH / 8 + 0.5 + 2,
},
]}
>
{typeof routeTitle !== "string" ? (
routeTitle.titleComponent(open, isPermanent)
) : (
<Typography
variant="h6"
component="h1"
noWrap
sx={{
flexShrink: 0,
flexGrow: 1,
typography: { sm: "h5" },
textAlign: { xs: "left", sm: "center" },
}}
>
{title}
</Typography>
)}
</Box>
</Fade>
<HelpMenu />
<UserMenu />
</Toolbar>
</AppBar>
);
}

View File

@@ -66,7 +66,6 @@ export default function UserMenu(props: IconButtonProps) {
aria-label="Open user menu"
aria-controls="user-menu"
aria-haspopup="true"
edge="end"
size="large"
{...props}
ref={anchorEl}

View File

@@ -39,7 +39,7 @@ import {
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
import useBasicSearch from "@src/hooks/useBasicSearch";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
const SEARCH_KEYS = ["id", "name", "section", "description"];
@@ -112,7 +112,7 @@ export default function HomePage() {
message="No tables"
description="There are no tables in this project. Sign in with an ADMIN account to create tables."
fullScreen
style={{ marginTop: -APP_BAR_HEIGHT }}
style={{ marginTop: -TOP_BAR_HEIGHT }}
/>
);
}

View File

@@ -6,7 +6,8 @@ import { Go as GoIcon } from "@src/assets/icons";
import HomeIcon from "@mui/icons-material/HomeOutlined";
import AuthLayout from "@src/layouts/AuthLayout";
import Navigation, { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import Navigation from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import EmptyState from "@src/components/EmptyState";
import meta from "@root/package.json";
@@ -33,7 +34,7 @@ export default function NotFound() {
Home
</Button>
}
style={{ marginTop: -APP_BAR_HEIGHT }}
style={{ marginTop: -TOP_BAR_HEIGHT }}
/>
</Navigation>
);

View File

@@ -22,7 +22,7 @@ import {
tableSchemaAtom,
} from "@src/atoms/tableScope";
import { ROUTES } from "@src/constants/routes";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
/**
@@ -92,15 +92,15 @@ export default function ProvidedSubTablePage() {
backgroundImage: "none",
},
"& .modal-title-row": {
height: APP_BAR_HEIGHT,
height: TOP_BAR_HEIGHT,
"& .MuiDialogTitle-root": {
px: 2,
py: (APP_BAR_HEIGHT - 28) / 2 / 8,
py: (TOP_BAR_HEIGHT - 28) / 2 / 8,
},
"& .dialog-close": { m: (APP_BAR_HEIGHT - 40) / 2 / 8, ml: -1 },
"& .dialog-close": { m: (TOP_BAR_HEIGHT - 40) / 2 / 8, ml: -1 },
},
"& .table-container": {
height: `calc(100vh - ${APP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px - 16px)`,
height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px - 16px)`,
},
}}
ScrollableDialogContentProps={{

View File

@@ -39,7 +39,7 @@ import {
import { TableSettings } from "@src/types/table";
import { ROUTES } from "@src/constants/routes";
import useBasicSearch from "@src/hooks/useBasicSearch";
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
const SEARCH_KEYS = ["id", "name", "section", "description"];
@@ -112,7 +112,7 @@ export default function HomePage() {
message="No tables"
description="There are no tables in this project. Sign in with an ADMIN account to create tables."
fullScreen
style={{ marginTop: -APP_BAR_HEIGHT }}
style={{ marginTop: -TOP_BAR_HEIGHT }}
/>
);
}
@@ -165,7 +165,10 @@ export default function HomePage() {
label="Search tables"
onChange={(e) => handleQuery(e.target.value)}
paperSx={{
maxWidth: (theme) => ({ md: theme.breakpoints.values.sm - 48 }),
maxWidth: (theme) => ({
md: theme.breakpoints.values.sm - 48 * 4,
lg: theme.breakpoints.values.sm - 48,
}),
mb: { xs: 2, md: -6 },
}}
/>