consolidate Navigation, add settings pages stubs

This commit is contained in:
Sidney Alcantara
2021-09-02 14:34:40 +10:00
parent 5ca534bd9c
commit a838553682
25 changed files with 271 additions and 1204 deletions

View File

@@ -18,7 +18,7 @@
"@material-ui/icons": "^5.0.0-beta.4",
"@material-ui/lab": "^5.0.0-alpha.43",
"@material-ui/styles": "^5.0.0-beta.4",
"@mdi/js": "^5.8.55",
"@mdi/js": "^5.9.55",
"@monaco-editor/react": "^4.1.0",
"@tinymce/tinymce-react": "^3.4.0",
"algoliasearch": "^4.8.6",

View File

@@ -1,5 +1,5 @@
import { lazy, Suspense } from "react";
import { Route, Switch, Link } from "react-router-dom";
import { Route, Switch, Link, Redirect } from "react-router-dom";
import { StyledEngineProvider } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
@@ -25,28 +25,22 @@ import TestView from "pages/Test";
import Favicon from "assets/Favicon";
import "analytics";
const AuthSetupGuidePage = lazy(
() => import("pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */)
);
// prettier-ignore
const AuthSetupGuidePage = lazy(() => import("pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */));
// prettier-ignore
const ImpersonatorAuthPage = lazy(() => import("./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */));
// prettier-ignore
const JwtAuthPage = lazy(() => import("./pages/Auth/JwtAuth" /* webpackChunkName: "JwtAuthPage" */));
const HomePage = lazy(
() => import("./pages/Home" /* webpackChunkName: "HomePage" */)
);
const TablePage = lazy(
() => import("./pages/Table" /* webpackChunkName: "TablePage" */)
);
const ImpersonatorAuthPage = lazy(
() =>
import(
"./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */
)
);
const JwtAuthPage = lazy(
() => import("./pages/Auth/JwtAuth" /* webpackChunkName: "JwtAuthPage" */)
);
// const GridView = lazy(
// () => import("./views/GridView" /* webpackChunkName: "GridView" */)
// );
// prettier-ignore
const HomePage = lazy(() => import("./pages/Home" /* webpackChunkName: "HomePage" */));
// prettier-ignore
const TablePage = lazy(() => import("./pages/Table" /* webpackChunkName: "TablePage" */));
// prettier-ignore
const ProjectSettingsPage = lazy(() => import("./pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
// prettier-ignore
const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
export default function App() {
return (
@@ -85,7 +79,9 @@ export default function App() {
path={routes.signOut}
render={() => <SignOutView />}
/>
<Route exact path={"/test"} render={() => <TestView />} />
<PrivateRoute
exact
path={[
@@ -93,6 +89,9 @@ export default function App() {
routes.tableWithId,
routes.tableGroupWithId,
routes.gridWithId,
routes.settings,
routes.projectSettings,
routes.userSettings,
]}
render={() => (
<RowyContextProvider>
@@ -110,6 +109,24 @@ export default function App() {
path={routes.tableGroupWithId}
render={() => <TablePage />}
/>
<PrivateRoute
exact
path={routes.settings}
render={() => (
<Redirect to={routes.userSettings} />
)}
/>
<PrivateRoute
exact
path={routes.projectSettings}
render={() => <ProjectSettingsPage />}
/>
<PrivateRoute
exact
path={routes.userSettings}
render={() => <UserSettingsPage />}
/>
</Switch>
</RowyContextProvider>
)}

View File

@@ -0,0 +1,10 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
import { mdiFirebase } from '@mdi/js';
export default function AddColumn(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d={mdiFirebase} />
</SvgIcon>
);
}

View File

@@ -0,0 +1,10 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
import { mdiWrenchOutline } from "@mdi/js";
export default function ProjectSettings(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d={mdiWrenchOutline} />
</SvgIcon>
);
}

View File

@@ -83,8 +83,8 @@ export default function AuthLayout({ children, loading }: IAuthLayoutProps) {
<Div100vh className={classes.root} style={{ minHeight: "100rvh" }}>
<Paper className={classes.paper}>
<Logo />
<Typography variant="overline" className={classes.projectName}>
{process.env.REACT_APP_FIREBASE_PROJECT_ID}
<Typography variant="body2" className={classes.projectName}>
Project: {process.env.REACT_APP_FIREBASE_PROJECT_ID}
</Typography>
{children}

View File

@@ -1,239 +0,0 @@
import React, { useState, useEffect } from "react";
import { SearchIndex } from "algoliasearch/lite";
import { FacetHit } from "@algolia/client-search";
import useAlgolia from "use-algolia";
import { useDebouncedCallback } from "use-debounce";
import { makeStyles, createStyles } from "@material-ui/styles";
import {
Grid,
Typography,
Button,
TextField,
InputAdornment,
ListItemSecondaryAction,
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import MultiSelect from "@antlerengineering/multiselect";
const useStyles = makeStyles((theme) =>
createStyles({
resetFilters: { marginRight: -theme.spacing(1) },
filterGrid: {
marginTop: 0,
marginBottom: theme.spacing(3),
},
listItemText: { whiteSpace: "pre-line" },
count: {
position: "static",
marginLeft: "auto",
paddingLeft: theme.spacing(1.5),
transform: "none",
color: theme.palette.text.disabled,
},
})
);
/**
* Generates the string to dispatch as filters for the query
* @param filterValues The user-selected filters
* @param requiredFilters Filters not selected by the user
*/
const generateFiltersString = (
filterValues: Record<string, string[]>,
requiredFilters?: string
) => {
if (Object.keys(filterValues).length === 0) return null;
let filtersString = Object.entries(filterValues)
.filter(([, values]) => values.length > 0)
.map(
([facet, values]) =>
`(${values
.map((value) => `${facet}:"${value.replace(/"/g, '\\"')}"`)
.join(" OR ")})`
)
.join(" AND ");
if (requiredFilters) {
if (filtersString)
filtersString = requiredFilters + " AND " + filtersString;
else filtersString = requiredFilters;
}
return filtersString;
};
export interface IAlgoliaFiltersProps {
index: SearchIndex;
request: ReturnType<typeof useAlgolia>[0]["request"];
requestDispatch: ReturnType<typeof useAlgolia>[1];
requiredFilters?: string;
label: string;
filters: {
label: string;
facet: string;
labelTransformer?: (value: string) => string;
}[];
search?: boolean;
}
export default function AlgoliaFilters({
index,
request,
requestDispatch,
requiredFilters,
label,
filters,
search = true,
}: IAlgoliaFiltersProps) {
const classes = useStyles();
// Store filter values
const [filterValues, setFilterValues] = useState<Record<string, string[]>>(
{}
);
// Push filter values to dispatch
useEffect(() => {
const filtersString = generateFiltersString(filterValues, requiredFilters);
if (filtersString === null) return;
requestDispatch({ filters: filtersString });
}, [filterValues]);
// Store facet values
const [facetValues, setFacetValues] = useState<
Record<string, readonly FacetHit[]>
>({});
// Get facet values
useEffect(() => {
if (!index) return;
filters.forEach((filter) => {
const params = { ...request, maxFacetHits: 100 };
// Ignore current user-selected value for these filters so all options
// continue to show up
params.filters =
generateFiltersString(
{ ...filterValues, [filter.facet]: [] },
requiredFilters
) ?? "";
index
.searchForFacetValues(filter.facet, "", params)
.then(({ facetHits }) =>
setFacetValues((other) => ({ ...other, [filter.facet]: facetHits }))
);
});
}, [filters, index, filterValues, requiredFilters]);
// Reset filters
const handleResetFilters = () => {
setFilterValues({});
setQuery("");
requestDispatch({ filters: requiredFilters ?? "", query: "" });
};
// Store search query
const [query, setQuery] = useState("");
const [handleQueryChange] = useDebouncedCallback(
(query: string) => requestDispatch({ query }),
500
);
return (
<div>
<Grid container spacing={1} alignItems="center">
<Grid item xs>
<Typography variant="overline">
Filter{label ? " " + label : "s"}
</Typography>
</Grid>
<Grid item>
<Button
color="primary"
onClick={handleResetFilters}
className={classes.resetFilters}
disabled={query === "" && Object.keys(filterValues).length === 0}
>
Reset Filters
</Button>
</Grid>
</Grid>
<Grid
container
spacing={2}
alignItems="center"
className={classes.filterGrid}
>
{search && (
<Grid item xs={12} md={4} lg={3}>
<TextField
value={query}
onChange={(e) => {
setQuery(e.target.value);
handleQueryChange(e.target.value);
}}
variant="filled"
type="search"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
aria-label={`Search${label ? " " + label : ""}`}
placeholder={`Search${label ? " " + label : ""}`}
hiddenLabel
fullWidth
/>
</Grid>
)}
{filters.map((filter) => (
<Grid item key={filter.facet} xs={12} sm={6} md={4} lg={3}>
<MultiSelect
label={filter.label}
value={filterValues[filter.facet] ?? []}
onChange={(value) =>
setFilterValues((other) => ({
...other,
[filter.facet]: value,
}))
}
options={
facetValues[filter.facet]?.map((item) => ({
value: item.value,
label: filter.labelTransformer
? filter.labelTransformer(item.value)
: item.value,
count: item.count,
})) ?? []
}
itemRenderer={(option) => (
<React.Fragment key={option.value}>
{option.label}
<ListItemSecondaryAction className={classes.count}>
<Typography
variant="body2"
color="inherit"
component="span"
>
{(option as any).count}
</Typography>
</ListItemSecondaryAction>
</React.Fragment>
)}
searchable={facetValues[filter.facet]?.length > 10}
/>
</Grid>
))}
</Grid>
</div>
);
}

View File

@@ -1,249 +0,0 @@
import React, { useState } from "react";
import clsx from "clsx";
import {
Card,
Grid,
Typography,
Button,
CardActions,
CardContent,
CardMedia,
Divider,
Tabs,
Tab,
} from "@material-ui/core";
import { ButtonProps } from "@material-ui/core/Button";
import GoIcon from "assets/icons/Go";
import useStyles from "./styles";
export interface ICardProps {
className?: string;
style?: React.CSSProperties;
overline?: React.ReactNode;
title?: React.ReactNode;
imageSource?: string;
imageShape?: "square" | "circle";
imageClassName?: string;
tabs?: {
label: string;
content: React.ReactNode;
disabled?: boolean;
}[];
bodyContent?: React.ReactNode;
primaryButton?: { label: string } & Partial<ButtonProps>;
primaryLink?: {
href?: string;
target?: string;
rel?: string;
label: string;
} & Partial<ButtonProps>;
secondaryAction?: React.ReactNode;
}
const a11yProps = (index: number) => ({
id: `full-width-tab-${index}`,
"aria-controls": `full-width-tabpanel-${index}`,
});
export default function BasicCard({
className,
style,
overline,
title,
imageSource,
imageShape = "square",
imageClassName,
tabs,
bodyContent,
primaryButton,
primaryLink,
secondaryAction,
}: ICardProps) {
const classes = useStyles();
const [tab, setTab] = useState(0);
const handleChangeTab = (event: React.ChangeEvent<{}>, newValue: number) =>
setTab(newValue);
return (
<Card className={clsx(classes.root, className)} style={style}>
<Grid
container
direction="column"
wrap="nowrap"
className={classes.container}
>
<Grid item xs className={classes.cardContentContainer}>
<CardContent className={clsx(classes.container, classes.cardContent)}>
<Grid
container
direction="column"
wrap="nowrap"
className={classes.container}
>
{(overline || title || imageSource) && (
<Grid item className={classes.headerContainer}>
<Grid container spacing={3}>
<Grid item xs>
{overline && (
<Typography
variant="overline"
className={classes.overline}
>
{overline}
</Typography>
)}
{title && (
<Typography variant="h5" className={classes.title}>
{title}
</Typography>
)}
</Grid>
{imageSource && (
<Grid item>
<CardMedia
className={clsx(
classes.image,
imageShape === "circle" && classes.imageCircle,
imageClassName
)}
image={imageSource}
title={typeof title === "string" ? title : ""}
/>
</Grid>
)}
</Grid>
</Grid>
)}
{tabs && (
<Grid item className={classes.tabsContainer}>
<Tabs
className={classes.tabs}
value={tab}
onChange={handleChangeTab}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="full width tabs"
>
{tabs?.map((tab, index) => (
<Tab
key={`card-tab-${index}`}
className={classes.tab}
label={tab.label}
disabled={tab.disabled}
{...a11yProps(index)}
/>
))}
</Tabs>
<Divider className={clsx(classes.tabs, classes.tabDivider)} />
</Grid>
)}
{(tabs || bodyContent) && (
<Grid item xs className={classes.contentContainer}>
{tabs && (
<div className={classes.tabSection}>
{tabs[tab].content && Array.isArray(tabs[tab].content) ? (
<Grid
container
direction="column"
wrap="nowrap"
justifyContent="space-between"
spacing={3}
className={classes.tabContentGrid}
>
{(tabs[tab].content as React.ReactNode[]).map(
(element, index) => (
<Grid item key={`tab-content-${index}`}>
{element}
</Grid>
)
)}
</Grid>
) : (
tabs[tab].content
)}
</div>
)}
{bodyContent && Array.isArray(bodyContent) ? (
<Grid
container
direction="column"
wrap="nowrap"
justifyContent="space-between"
className={classes.container}
>
{bodyContent.map((element, i) => (
<Grid item key={i}>
{element}
</Grid>
))}
</Grid>
) : (
bodyContent
)}
</Grid>
)}
</Grid>
</CardContent>
</Grid>
{(primaryButton || primaryLink || secondaryAction) && (
<Grid item>
<Divider className={classes.divider} />
<CardActions className={classes.cardActions}>
<Grid item>
{primaryButton && (
<Button
{...primaryButton}
color={primaryButton.color || "primary"}
disabled={!!primaryButton.disabled}
endIcon={
primaryButton.endIcon === undefined ? (
<GoIcon />
) : (
primaryButton.endIcon
)
}
>
{primaryButton.label}
</Button>
)}
{primaryLink && (
<Button
classes={{ label: classes.primaryLinkLabel }}
{...(primaryLink as any)}
color={primaryLink.color || "primary"}
component="a"
endIcon={
primaryLink.endIcon === undefined ? (
<GoIcon />
) : (
primaryLink.endIcon
)
}
>
{primaryLink.label}
</Button>
)}
</Grid>
{secondaryAction && <Grid item>{secondaryAction}</Grid>}
</CardActions>
</Grid>
)}
</Grid>
</Card>
);
}

View File

@@ -1,58 +0,0 @@
import { makeStyles, createStyles } from "@material-ui/styles";
const useStyles = makeStyles((theme) =>
createStyles({
root: { width: "100%", height: "100%" },
container: { height: "100%" },
cardContentContainer: {
"&:last-child": { paddingBottom: theme.spacing(3) },
},
cardContent: {
"&:last-child": { paddingBottom: 0 },
},
headerContainer: {},
tabsContainer: { "$headerContainer + &": { marginTop: theme.spacing(2) } },
contentContainer: {
"$headerContainer + &": { marginTop: theme.spacing(2) },
},
overline: {
marginBottom: theme.spacing(3),
color: theme.palette.text.disabled,
wordBreak: "break-word",
},
title: {
whiteSpace: "pre-line",
wordBreak: "break-word",
},
image: {
width: 80,
height: 80,
borderRadius: theme.shape.borderRadius,
},
imageCircle: { borderRadius: "50%" },
tabs: { margin: theme.spacing(0, -2) },
tab: { minWidth: 0 },
tabDivider: { marginTop: -1 },
tabSection: { paddingTop: theme.spacing(2), height: "100%" },
tabContentGrid: { height: `calc(100% + ${theme.spacing(3)})` },
divider: {
margin: theme.spacing(2),
marginBottom: 0,
},
cardActions: {
padding: theme.spacing(0.75, 1),
display: "flex",
justifyContent: "space-between",
},
primaryLinkLabel: { whiteSpace: "nowrap" },
})
);
export default useStyles;

View File

@@ -1,81 +0,0 @@
import { Grid as MuiGrid } from "@material-ui/core";
import Card from "./Card";
import useAlgolia from "use-algolia";
import AlgoliaFilters from "./AlgoliaFilters";
import _get from "lodash/get";
const advisorsFilters = [
{ label: "Type", facet: "type" },
{ label: "Experience (Industry)", facet: "expertise" },
{ label: "Location", facet: "location" },
];
interface IGridProps {
collection: string;
filters: any[];
}
export const replacer = (data: any) => (m: string, key: string) => {
const objKey = key.split(":")[0];
const defaultValue = key.split(":")[1] || "";
return _get(data, objKey, defaultValue);
};
const CARD_CONFIG = {
title: `{{firstName}} {{lastName}}`,
image: "{{profilePhoto[0].downloadURL}}",
body: "{{bio}}",
};
export default function Grid({ collection }: IGridProps) {
const [algoliaState, requestDispatch, ,] = useAlgolia(
process.env.REACT_APP_ALGOLIA_APP_ID!,
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY!,
collection,
{ hitsPerPage: 100 }
);
const isLoading = algoliaState.loading || !algoliaState.index;
const noResults = algoliaState.hits.length === 0;
const requiredFilters = ``;
const isEmpty =
noResults &&
algoliaState.request?.query === undefined &&
algoliaState.request?.filters === requiredFilters;
return (
<>
{algoliaState.index && !isEmpty && (
<AlgoliaFilters
index={algoliaState.index}
request={algoliaState.request}
requestDispatch={requestDispatch}
requiredFilters={requiredFilters}
label={collection}
filters={[]}
search
/>
)}
<MuiGrid container spacing={4}>
{" "}
{algoliaState.hits.map((hit) => {
return (
<MuiGrid key={hit.objectID} item lg={4} md={6} xs={12}>
{" "}
<Card
bodyContent={CARD_CONFIG.body.replace(
/\{\{(.*?)\}\}/g,
replacer(hit)
)}
title={CARD_CONFIG.title.replace(
/\{\{(.*?)\}\}/g,
replacer(hit)
)}
imageSource={CARD_CONFIG.image.replace(
/\{\{(.*?)\}\}/g,
replacer(hit)
)}
/>{" "}
</MuiGrid>
);
})}
</MuiGrid>
</>
);
}

View File

@@ -1,131 +0,0 @@
import { makeStyles, createStyles } from "@material-ui/styles";
import {
useTheme,
useMediaQuery,
Drawer,
DrawerProps,
Grid,
IconButton,
List,
MenuItem,
ListItemIcon,
ListItemText,
} from "@material-ui/core";
import CloseIcon from "assets/icons/Backburger";
import AddIcon from "@material-ui/icons/Add";
import { APP_BAR_HEIGHT } from ".";
import Logo from "assets/Logo";
import { useRowyContext } from "contexts/RowyContext";
import useRouter from "hooks/useRouter";
export const NAV_DRAWER_WIDTH = 300;
const useStyles = makeStyles((theme) =>
createStyles({
paper: {
width: NAV_DRAWER_WIDTH,
overflowX: "hidden",
backgroundColor: theme.palette.background.paper,
},
logoRow: {
height: APP_BAR_HEIGHT,
marginTop: 0,
marginBottom: theme.spacing(1),
padding: theme.spacing(0, 2),
},
logo: { marginLeft: theme.spacing(1.5) },
nav: { height: "100%" },
list: {
display: "flex",
flexDirection: "column",
flexWrap: "nowrap",
height: "100%",
},
createTable: { marginTop: "auto" },
})
);
export interface INavDrawerProps extends DrawerProps {
handleCreateTable: () => void;
}
export default function NavDrawer({
handleCreateTable,
...props
}: INavDrawerProps) {
const classes = useStyles();
const theme = useTheme();
const isSm = useMediaQuery(theme.breakpoints.down("md"));
const { sections } = useRowyContext();
const { location } = useRouter();
const { hash } = location;
return (
<Drawer
open
variant={isSm ? "temporary" : "persistent"}
{...props}
classes={{ paper: classes.paper }}
>
<Grid
container
spacing={1}
wrap="nowrap"
alignItems="center"
className={classes.logoRow}
>
<Grid item>
<IconButton
aria-label="Close navigation drawer"
onClick={props.onClose as any}
edge="start"
size="large"
>
<CloseIcon />
</IconButton>
</Grid>
<Grid item className={classes.logo}>
<Logo />
</Grid>
</Grid>
<nav className={classes.nav}>
<List className={classes.list}>
{sections &&
Object.keys(sections).map((section) => (
<li key={section}>
<MenuItem
component="a"
href={`#${section}`}
selected={
section === decodeURIComponent(hash.replace("#", ""))
}
onClick={isSm ? (props.onClose as any) : undefined}
>
<ListItemText primary={section} />
</MenuItem>
</li>
))}
<li className={classes.createTable}>
<MenuItem onClick={handleCreateTable}>
<ListItemIcon>
<AddIcon />
</ListItemIcon>
<ListItemText primary="Create Table" />
</MenuItem>
</li>
</List>
</nav>
</Drawer>
);
}

View File

@@ -1,173 +0,0 @@
import clsx from "clsx";
import { makeStyles, createStyles } from "@material-ui/styles";
import {
useScrollTrigger,
Grid,
AppBar,
Toolbar,
IconButton,
} from "@material-ui/core";
import MenuIcon from "@material-ui/icons/Menu";
import Logo from "assets/Logo";
import NavDrawer, { NAV_DRAWER_WIDTH } from "./NavDrawer";
import UserMenu from "components/Navigation/UserMenu";
export const APP_BAR_HEIGHT = 56;
const useStyles = makeStyles((theme) =>
createStyles({
open: {},
navDrawerContainer: {
[theme.breakpoints.up("md")]: {
width: 0,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"$open &": {
width: NAV_DRAWER_WIDTH,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
},
},
appBar: {
height: APP_BAR_HEIGHT,
[theme.breakpoints.down("md")]: { paddingRight: 0 },
[theme.breakpoints.up("md")]: {
transition:
theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}) +
", " +
theme.transitions.create(["box-shadow", "background-color"]),
"$open &": {
width: `calc(100% - ${NAV_DRAWER_WIDTH}px)`,
transition:
theme.transitions.create("width", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}) +
", " +
theme.transitions.create(["box-shadow", "background-color"]),
},
},
// Elevation 8
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))",
"&::before": {
content: "''",
display: "block",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create("opacity"),
},
},
appBarScrolled: {
"&::before": {
opacity: 0,
},
},
toolbar: {
height: APP_BAR_HEIGHT,
minHeight: "auto",
minWidth: 0,
maxWidth: "none",
padding: theme.spacing(0, 2),
},
openButton: {
opacity: 1,
transition: theme.transitions.create("opacity"),
"$open &": { opacity: 0 },
},
logo: {
flex: 1,
textAlign: "center",
opacity: 1,
transition: theme.transitions.create("opacity"),
"$open &": { opacity: 0 },
},
})
);
export interface IHomeNavigationProps {
children: React.ReactNode;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleCreateTable: () => void;
}
export default function HomeNavigation({
children,
open,
setOpen,
handleCreateTable,
}: IHomeNavigationProps) {
const classes = useStyles();
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
return (
<Grid
container
wrap="nowrap"
alignItems="flex-start"
className={clsx(open && classes.open)}
>
<Grid item className={classes.navDrawerContainer}>
<NavDrawer
open={open}
onClose={() => setOpen(false)}
handleCreateTable={handleCreateTable}
/>
</Grid>
<Grid item xs>
<AppBar
color="inherit"
elevation={trigger ? 1 : 0}
className={clsx(classes.appBar, trigger && classes.appBarScrolled)}
>
<Toolbar className={classes.toolbar}>
<IconButton
aria-label="Open navigation drawer"
onClick={() => setOpen(true)}
edge="start"
size="large"
className={classes.openButton}
>
<MenuIcon />
</IconButton>
<div className={classes.logo}>
<Logo />
</div>
<UserMenu />
</Toolbar>
</AppBar>
{children}
</Grid>
</Grid>
);
}

View File

@@ -13,6 +13,7 @@ import {
} from "@material-ui/core";
import HomeIcon from "@material-ui/icons/HomeOutlined";
import SettingsIcon from "@material-ui/icons/SettingsOutlined";
import ProjectSettingsIcon from "assets/icons/ProjectSettings";
import CloseIcon from "assets/icons/Backburger";
import { APP_BAR_HEIGHT } from ".";
@@ -26,19 +27,21 @@ export const NAV_DRAWER_WIDTH = 256;
export interface INavDrawerProps extends DrawerProps {
currentSection?: string;
currentTable: string;
currentTable?: string;
onClose: NonNullable<DrawerProps["onClose"]>;
}
export default function NavDrawer({
currentSection,
currentTable,
currentTable = "",
...props
}: INavDrawerProps) {
const { sections } = useRowyContext();
const { userClaims, sections } = useRowyContext();
const closeDrawer = (e: {}) => props.onClose(e, "escapeKeyDown");
return (
<Drawer
open
{...props}
sx={{ "& .MuiDrawer-paper": { minWidth: NAV_DRAWER_WIDTH } }}
>
@@ -62,7 +65,7 @@ export default function NavDrawer({
<nav>
<List disablePadding>
<li>
<MenuItem component={Link} to={routes.home}>
<MenuItem component={Link} to={routes.home} onClick={closeDrawer}>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
@@ -70,13 +73,31 @@ export default function NavDrawer({
</MenuItem>
</li>
<li>
<MenuItem component={Link} to={routes.settings}>
<MenuItem
component={Link}
to={routes.settings}
onClick={closeDrawer}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</MenuItem>
</li>
{userClaims?.roles?.includes("ADMIN") && (
<li>
<MenuItem
component={Link}
to={routes.projectSettings}
onClick={closeDrawer}
>
<ListItemIcon>
<ProjectSettingsIcon />
</ListItemIcon>
<ListItemText primary="Project Settings" />
</MenuItem>
</li>
)}
<Divider variant="middle" sx={{ mt: 1, mb: 1 }} />
@@ -88,6 +109,7 @@ export default function NavDrawer({
tables={tables}
currentSection={currentSection}
currentTable={currentTable}
closeDrawer={closeDrawer}
/>
))}
</List>

View File

@@ -53,6 +53,8 @@ export interface INavDrawerItemProps {
currentSection?: string;
currentTable: string;
closeDrawer: (e: {}) => void;
}
export default function NavDrawerItem({
@@ -60,6 +62,7 @@ export default function NavDrawerItem({
tables,
currentSection,
currentTable,
closeDrawer,
}: INavDrawerItemProps) {
const classes = useStyles();
@@ -111,6 +114,7 @@ export default function NavDrawerItem({
"~2F"
)}`
}
onClick={closeDrawer}
>
<ListItemText
primary={table.name}

View File

@@ -1,7 +1,6 @@
import { useState, useRef } from "react";
import { Link } from "react-router-dom";
import { makeStyles, createStyles } from "@material-ui/styles";
import {
IconButton,
IconButtonProps,
@@ -14,59 +13,19 @@ import {
ListItemSecondaryAction,
Link as MuiLink,
Divider,
Badge,
} from "@material-ui/core";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
import UpdateChecker, { useLatestUpdateState } from "./UpdateChecker";
import { useAppContext } from "contexts/AppContext";
import routes from "constants/routes";
import { projectId } from "@src/firebase";
import meta from "@root/package.json";
const useStyles = makeStyles((theme) =>
createStyles({
spacer: {
width: 48,
height: 48,
},
iconButton: {},
avatar: {
"$iconButton &": {
width: 24,
height: 24,
},
},
paper: { minWidth: 160 },
// divider: { margin: theme.spacing(1, 2) },
secondaryAction: { pointerEvents: "none" },
secondaryIcon: {
display: "block",
color: theme.palette.action.active,
},
subMenu: { marginTop: theme.spacing(-0.5) },
version: {
userSelect: "none",
color: theme.palette.text.disabled,
margin: 0,
},
})
);
export default function UserMenu(props: IconButtonProps) {
const classes = useStyles();
const anchorEl = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const [themeSubMenu, setThemeSubMenu] = useState<EventTarget | null>(null);
const [latestUpdate] = useLatestUpdateState<null | Record<string, any>>();
const {
currentUser,
@@ -77,14 +36,14 @@ export default function UserMenu(props: IconButtonProps) {
setThemeOverridden,
} = useAppContext();
if (!currentUser || !userDoc || !userDoc?.state?.doc)
return <div className={classes.spacer} />;
return <div style={{ width: 48, height: 48 }} />;
const displayName = userDoc?.state?.doc?.user?.displayName;
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;
const email = userDoc?.state?.doc?.user?.email;
const avatar = avatarUrl ? (
<Avatar src={avatarUrl} className={classes.avatar} />
<Avatar src={avatarUrl} />
) : (
<AccountCircleIcon color="secondary" />
);
@@ -122,15 +81,9 @@ export default function UserMenu(props: IconButtonProps) {
{...props}
ref={anchorEl}
onClick={() => setOpen(true)}
className={classes.iconButton}
sx={{ "& .MuiAvatar-root": { width: 24, height: 24 } }}
>
{latestUpdate?.tag_name > "v" + meta.version ? (
<Badge color="primary" overlap="circular" variant="dot">
{avatar}
</Badge>
) : (
avatar
)}
{avatar}
</IconButton>
<Menu
@@ -141,19 +94,19 @@ export default function UserMenu(props: IconButtonProps) {
transformOrigin={{ vertical: "top", horizontal: "right" }}
open={open}
onClose={() => setOpen(false)}
classes={{ paper: classes.paper }}
sx={{ "& .MuiPaper-root": { minWidth: 160 } }}
>
<ListItem>
<ListItem style={{ cursor: "default" }}>
<ListItemAvatar>{avatar}</ListItemAvatar>
<ListItemText primary={displayName} secondary={email} />
</ListItem>
<Divider variant="middle" />
<Divider variant="middle" sx={{ mt: 0.5, mb: 0.5 }} />
<MenuItem onClick={(e) => setThemeSubMenu(e.target)}>
Theme
<ListItemSecondaryAction className={classes.secondaryAction}>
<ArrowRightIcon className={classes.secondaryIcon} />
<ListItemSecondaryAction style={{ pointerEvents: "none" }}>
<ArrowRightIcon style={{ display: "block" }} />
</ListItemSecondaryAction>
</MenuItem>
@@ -165,7 +118,7 @@ export default function UserMenu(props: IconButtonProps) {
transformOrigin={{ vertical: "top", horizontal: "right" }}
open
onClose={() => setThemeSubMenu(null)}
classes={{ paper: classes.subMenu }}
sx={{ "& .MuiPaper-root": { mt: -0.5 } }}
>
<MenuItem
onClick={() => changeTheme("system")}
@@ -189,7 +142,7 @@ export default function UserMenu(props: IconButtonProps) {
)}
<MenuItem component={Link} to={routes.userSettings} disabled>
User settings
Settings
</MenuItem>
<Divider variant="middle" />
@@ -200,12 +153,6 @@ export default function UserMenu(props: IconButtonProps) {
<Divider variant="middle" />
<MenuItem component={Link} to={routes.projectSettings} disabled>
Project settings
</MenuItem>
<UpdateChecker />
<ListItem>
<ListItemText
primary={
@@ -237,7 +184,11 @@ export default function UserMenu(props: IconButtonProps) {
}
primaryTypographyProps={{ variant: "caption", color: "inherit" }}
secondaryTypographyProps={{ variant: "caption", color: "inherit" }}
className={classes.version}
sx={{
userSelect: "none",
color: "text.disabled",
margin: 0,
}}
/>
</ListItem>
</Menu>

View File

@@ -1,72 +1,29 @@
import React, { useState, useEffect } from "react";
import _find from "lodash/find";
import { ReactNode, useState } from "react";
import { makeStyles, createStyles } from "@material-ui/styles";
import { AppBar, Toolbar, IconButton } from "@material-ui/core";
import {
useScrollTrigger,
AppBar,
Toolbar,
IconButton,
Box,
Typography,
} from "@material-ui/core";
import MenuIcon from "@material-ui/icons/Menu";
import Breadcrumbs from "./Breadcrumbs";
import NavDrawer from "./NavDrawer";
// import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
import { useRowyContext } from "contexts/RowyContext";
import UserMenu from "./UserMenu";
import { name } from "@root/package.json";
import { projectId } from "@src/firebase";
export const APP_BAR_HEIGHT = 56;
const useStyles = makeStyles((theme) =>
createStyles({
appBar: {
// paddingRight: DRAWER_COLLAPSED_WIDTH,
height: APP_BAR_HEIGHT,
[theme.breakpoints.down("md")]: { paddingRight: 0 },
backgroundColor: theme.palette.background.default,
},
toolbar: {
height: APP_BAR_HEIGHT,
minHeight: "auto",
minWidth: 0,
maxWidth: "none",
padding: theme.spacing(0, 2),
},
breadcrumbs: { flex: 1 },
})
);
export default function Navigation({
children,
tableCollection,
}: React.PropsWithChildren<{ tableCollection: string }>) {
const { tables } = useRowyContext();
const classes = useStyles();
export interface INavigationProps {
children: ReactNode;
title?: ReactNode;
}
export default function Navigation({ children, title }: INavigationProps) {
const [open, setOpen] = useState(false);
useEffect(() => {
setOpen(false);
}, [tableCollection]);
// Find the matching section for the current route
const currentSection = _find(tables, [
"collection",
tableCollection?.split("/")[0],
])?.section;
const currentTable = tableCollection?.split("/")[0];
useEffect(() => {
const tableName =
_find(tables, ["collection", currentTable])?.name || currentTable;
document.title = `${tableName} | ${projectId} | ${name}`;
return () => {
document.title = `${projectId} | ${name}`;
};
}, [currentTable]);
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
return (
<>
@@ -74,9 +31,44 @@ export default function Navigation({
position="sticky"
color="inherit"
elevation={0}
className={classes.appBar}
className={trigger ? "scrolled" : ""}
sx={{
height: APP_BAR_HEIGHT, // Elevation 8
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))",
"&::before": {
content: "''",
display: "block",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
bgcolor: "background.default",
transition: (theme) => theme.transitions.create("opacity"),
},
"&:hover, &.scrolled": {
boxShadow: 1,
"&::before": { opacity: 0 },
},
}}
>
<Toolbar className={classes.toolbar}>
<Toolbar
sx={{
height: APP_BAR_HEIGHT,
minWidth: 0,
maxWidth: "none",
"&&": {
minHeight: APP_BAR_HEIGHT,
p: 0,
pl: 2,
pr: 2,
},
}}
>
<IconButton
aria-label="Open navigation drawer"
onClick={() => setOpen(true)}
@@ -86,19 +78,22 @@ export default function Navigation({
<MenuIcon />
</IconButton>
<Breadcrumbs className={classes.breadcrumbs} />
<Box sx={{ flex: 1, ml: 20 / 8, mr: 20 / 8, userSelect: "none" }}>
{typeof title === "string" ? (
<Typography variant="h6" component="h1">
{title}
</Typography>
) : (
title
)}
</Box>
<UserMenu />
{/* <Notifications /> */}
</Toolbar>
</AppBar>
<NavDrawer
currentSection={currentSection}
currentTable={currentTable}
open={open}
onClose={() => setOpen(false)}
/>
<NavDrawer open={open} onClose={() => setOpen(false)} />
{children}
</>

View File

@@ -1,4 +1,3 @@
import { makeStyles, createStyles } from "@material-ui/styles";
import { Stack, Button } from "@material-ui/core";
import { isCollectionGroup } from "utils/fns";
@@ -20,35 +19,6 @@ import { FieldType } from "constants/fields";
export const TABLE_HEADER_HEIGHT = 44;
const useStyles = makeStyles((theme) =>
createStyles({
root: {
width: "100%",
margin: 0,
padding: theme.spacing(0, 2, 1.5, 1),
height: TABLE_HEADER_HEIGHT,
overflowX: "auto",
whiteSpace: "nowrap",
userSelect: "none",
[theme.breakpoints.down("md")]: {
width: "100%",
paddingRight: theme.spacing(1),
},
"& > *": { paddingTop: "0 !important" },
},
spacer: { width: theme.spacing(2) },
midSpacer: { minWidth: theme.spacing(8) },
})
);
/**
* TODO: Make this properly mobile responsive, not just horizontally scrolling
*/
export default function TableHeader() {
const { currentUser } = useAppContext();
const { tableActions, tableState, userClaims } = useRowyContext();

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useContext } from "react";
import { auth, db } from "../firebase";
import { projectId, auth, db } from "@src/firebase";
import firebase from "firebase/app";
import useDoc from "hooks/useDoc";
import createPersistedState from "use-persisted-state";
@@ -13,6 +13,7 @@ import {
import Themes from "Themes";
import ErrorBoundary from "components/ErrorBoundary";
import { name } from "@root/package.json";
const useThemeState = createPersistedState("__ROWY__THEME");
const useThemeOverriddenState = createPersistedState(
@@ -50,6 +51,10 @@ export const AppProvider: React.FC = ({ children }) => {
});
}, []);
useEffect(() => {
document.title = `${projectId} | ${name}`;
}, []);
// Store matching userDoc
const [userDoc, dispatchUserDoc] = useDoc({});
// Get userDoc

View File

@@ -1,32 +1,38 @@
import { Typography, Button } from "@material-ui/core";
import { Button } from "@material-ui/core";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import AuthLayout from "components/Auth/AuthLayout";
import EmptyState from "components/EmptyState";
import FirebaseIcon from "assets/icons/Firebase";
import WIKI_LINKS from "constants/wikiLinks";
import { name } from "@root/package.json";
export default function AuthSetupGuide() {
return (
<AuthLayout>
<div>
<Typography variant="h6" component="h2" gutterBottom>
Firebase Authentication Not Set Up
</Typography>
<Typography variant="body1" color="textSecondary">
Firebase Authentication must be enabled to sign in to Rowy.
</Typography>
</div>
<Button
variant="contained"
endIcon={<OpenInNewIcon />}
component="a"
href={WIKI_LINKS.setUpAuth}
target="_blank"
rel="noopener"
>
Set-Up Guide
</Button>
<EmptyState
Icon={FirebaseIcon}
message="Set Up Firebase Authentication"
description={
<>
<span>
To sign in to {name}, set up Firebase Authentication in the
Firebase Console.
</span>
<Button
variant="contained"
color="primary"
endIcon={<OpenInNewIcon />}
href={WIKI_LINKS.setUpAuth}
target="_blank"
rel="noopener noreferrer"
>
Setup Guide
</Button>
</>
}
/>
</AuthLayout>
);
}

View File

@@ -1,38 +0,0 @@
import queryString from "query-string";
import { Hidden } from "@material-ui/core";
import Navigation from "components/Navigation";
import Grid from "components/Grid";
import SideDrawer from "components/SideDrawer";
import { RowyFilter } from "hooks/useRowy";
import useRouter from "hooks/useRouter";
export default function GridPage() {
const router = useRouter();
const tableCollection = decodeURIComponent(router.match.params.id);
let filters: RowyFilter[] = [];
const parsed = queryString.parse(router.location.search);
if (typeof parsed.filters === "string") {
// decoded
//[{"key":"cohort","operator":"==","value":"AMS1"}]
filters = JSON.parse(parsed.filters);
//TODO: json schema validator
}
return (
<Navigation tableCollection={tableCollection}>
<Grid
key={tableCollection}
collection={tableCollection}
filters={filters}
/>
<Hidden smDown>
<SideDrawer />
</Hidden>
</Navigation>
);
}

View File

@@ -19,7 +19,8 @@ import EditIcon from "@material-ui/icons/Edit";
import Favorite from "@material-ui/icons/Favorite";
import FavoriteBorder from "@material-ui/icons/FavoriteBorder";
import HomeNavigation from "components/HomeNavigation";
import Navigation from "components/Navigation";
import Logo from "assets/Logo";
import StyledCard from "components/StyledCard";
import routes from "constants/routes";
@@ -132,7 +133,6 @@ export default function HomePage() {
mode: TableSettingsDialogModes.create,
data: null,
});
const [open, setOpen] = useState(false);
const [openProjectSettings, setOpenProjectSettings] = useState(false);
const [openBuilderInstaller, setOpenBuilderInstaller] = useState(false);
@@ -178,15 +178,7 @@ export default function HomePage() {
const TableCard = ({ table }) => {
const checked = Boolean(_find(favs, table));
return (
<Grid
key={table.name}
item
xs={12}
sm={6}
md={open ? 6 : 4}
lg={4}
xl={3}
>
<Grid key={table.name} item xs={12} sm={6} md={4} lg={4} xl={3}>
<StyledCard
className={classes.card}
overline={table.section}
@@ -236,10 +228,12 @@ export default function HomePage() {
};
return (
<HomeNavigation
open={open}
setOpen={setOpen}
handleCreateTable={handleCreateTable}
<Navigation
title={
<div style={{ textAlign: "center" }}>
<Logo />
</div>
}
>
<main className={classes.root}>
{sections && Object.keys(sections).length > 0 ? (
@@ -353,6 +347,6 @@ export default function HomePage() {
{openBuilderInstaller && (
<BuilderInstaller handleClose={() => setOpenBuilderInstaller(false)} />
)}
</HomeNavigation>
</Navigation>
);
}

View File

@@ -0,0 +1,15 @@
import { Container, Typography } from "@material-ui/core";
import Navigation from "components/Navigation";
export default function ProjectSettings() {
return (
<Navigation title="Project Settings">
<Container style={{ height: "200vh" }}>
<Typography component="h2" variant="h4">
Project Settings
</Typography>
</Container>
</Navigation>
);
}

View File

@@ -0,0 +1,15 @@
import { Container, Typography } from "@material-ui/core";
import Navigation from "components/Navigation";
export default function UserSettings() {
return (
<Navigation title="Settings">
<Container>
<Typography component="h2" variant="h4">
Settings
</Typography>
</Container>
</Navigation>
);
}

View File

@@ -1,10 +1,12 @@
import { useEffect } from "react";
import queryString from "query-string";
import _isEmpty from "lodash/isEmpty";
import _find from "lodash/find";
import { Hidden } from "@material-ui/core";
import Navigation from "components/Navigation";
import Breadcrumbs from "components/Navigation/Breadcrumbs";
import Table from "components/Table";
import SideDrawer from "components/SideDrawer";
import TableHeaderSkeleton from "components/Table/Skeleton/TableHeaderSkeleton";
@@ -18,13 +20,33 @@ import useRouter from "hooks/useRouter";
import { DocActions } from "hooks/useDoc";
import ActionParamsProvider from "components/fields/Action/FormDialog/Provider";
import { name } from "@root/package.json";
import { projectId } from "@src/firebase";
export default function TablePage() {
const router = useRouter();
const tableCollection = decodeURIComponent(router.match.params.id);
const { tableState, tableActions, sideDrawerRef } = useRowyContext();
const { tableState, tableActions, sideDrawerRef, tables } = useRowyContext();
const { userDoc } = useAppContext();
// Find the matching section for the current route
const currentSection = _find(tables, [
"collection",
tableCollection?.split("/")[0],
])?.section;
const currentTable = tableCollection?.split("/")[0];
useEffect(() => {
const tableName =
_find(tables, ["collection", currentTable])?.name || currentTable;
document.title = `${tableName} | ${projectId} | ${name}`;
return () => {
document.title = `${projectId} | ${name}`;
};
}, [currentTable]);
let filters: RowyFilter[] = [];
const parsed = queryString.parse(router.location.search);
if (typeof parsed.filters === "string") {
@@ -54,7 +76,7 @@ export default function TablePage() {
if (!tableState) return null;
return (
<Navigation tableCollection={tableCollection}>
<Navigation title={<Breadcrumbs />}>
<ActionParamsProvider>
{tableState.loadingColumns && (
<>

View File

@@ -75,7 +75,7 @@ export default function TestView() {
const handleTabChange = (_, newTab) => setTab(newTab);
return (
<Navigation tableCollection="">
<Navigation title="Theme Test">
<Container style={{ margin: "24px 0 200px" }}>
<Stack spacing={8}>
<Table stickyHeader>

View File

@@ -2504,7 +2504,7 @@
prop-types "^15.7.2"
react-is "^17.0.2"
"@mdi/js@^5.8.55", "@mdi/js@^5.9.55":
"@mdi/js@^5.9.55":
version "5.9.55"
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-5.9.55.tgz#8f5bc4d924c23f30dab20545ddc768e778bbc882"
integrity sha512-BbeHMgeK2/vjdJIRnx12wvQ6s8xAYfvMmEAVsUx9b+7GiQGQ9Za8jpwp17dMKr9CgKRvemlAM4S7S3QOtEbp4A==