mirror of
https://github.com/rowyio/rowy.git
synced 2026-02-23 19:50:01 +01:00
rewrite home page, add useBasicSearch
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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<IAppBarProps> = () => {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
|
||||
|
||||
return (
|
||||
<MuiAppBar
|
||||
position="sticky"
|
||||
color="default"
|
||||
className={classes.appBar}
|
||||
elevation={trigger ? 4 : 0}
|
||||
>
|
||||
<Toolbar>
|
||||
<Grid item xs>
|
||||
<Logo />
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
component={Link}
|
||||
to={routes.signOut}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</Grid>
|
||||
</Toolbar>
|
||||
</MuiAppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppBar;
|
||||
@@ -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<FilledTextFieldProps> {
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
@@ -34,6 +39,7 @@ export default function FloatingSearch({
|
||||
type="search"
|
||||
id="user-management-search"
|
||||
size="medium"
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
@@ -46,16 +52,32 @@ export default function FloatingSearch({
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiInputLabel-root": {
|
||||
height: "0px",
|
||||
m: 0,
|
||||
p: 0,
|
||||
pointerEvents: "none",
|
||||
opacity: 0,
|
||||
mt: -5,
|
||||
mb: 2,
|
||||
},
|
||||
"& .MuiFilledInput-root": {
|
||||
boxShadow: 0,
|
||||
borderRadius: 2,
|
||||
"&::before": {
|
||||
borderRadius: 2,
|
||||
height: (theme) => (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,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
51
src/components/Home/AccessDenied.tsx
Normal file
51
src/components/Home/AccessDenied.tsx
Normal file
@@ -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 (
|
||||
<EmptyState
|
||||
fullScreen
|
||||
Icon={SecurityIcon}
|
||||
message="Access Denied"
|
||||
description={
|
||||
<>
|
||||
<Typography>
|
||||
You do not have access to this project. Please contact the project
|
||||
owner.
|
||||
</Typography>
|
||||
<Typography>
|
||||
If you are the project owner, please follow{" "}
|
||||
<MuiLink
|
||||
href={WIKI_LINKS.securityRules}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
these instructions
|
||||
</MuiLink>{" "}
|
||||
to set up the project’s security rules.
|
||||
</Typography>
|
||||
|
||||
<Button component={Link} to={routes.signOut}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
bgcolor: "background.default",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,7 @@ import { Zoom, Stack, Typography } from "@material-ui/core";
|
||||
|
||||
export default function HomeWelcomePrompt() {
|
||||
return (
|
||||
<Zoom
|
||||
in
|
||||
style={{
|
||||
transformOrigin: `${320 - 52}px ${320 - 52}px`,
|
||||
transitionDelay: "3s",
|
||||
}}
|
||||
>
|
||||
<Zoom in style={{ transformOrigin: `${320 - 52}px ${320 - 52}px` }}>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
76
src/components/Home/TableGrid/TableCard.tsx
Normal file
76
src/components/Home/TableGrid/TableCard.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
import GoIcon from "assets/icons/Go";
|
||||
|
||||
import { Table } from "contexts/RowyContext";
|
||||
|
||||
export interface ITableCardProps extends Table {
|
||||
link: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableCard({
|
||||
section,
|
||||
name,
|
||||
description,
|
||||
link,
|
||||
actions,
|
||||
}: ITableCardProps) {
|
||||
return (
|
||||
<Card style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardActionArea
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 2,
|
||||
}}
|
||||
component={Link}
|
||||
to={link}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="overline" component="p">
|
||||
{section}
|
||||
</Typography>
|
||||
<Typography variant="h6" component="h3" gutterBottom>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
minHeight: (theme) =>
|
||||
(theme.typography.body2.lineHeight as number) * 2 + "em",
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
|
||||
<CardActions>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
endIcon={<GoIcon />}
|
||||
component={Link}
|
||||
to={link}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
|
||||
{actions}
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
42
src/components/Home/TableGrid/TableCardSkeleton.tsx
Normal file
42
src/components/Home/TableGrid/TableCardSkeleton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Skeleton,
|
||||
} from "@material-ui/core";
|
||||
|
||||
export default function TableCardSkeleton() {
|
||||
return (
|
||||
<Card style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardContent style={{ flexGrow: 1 }}>
|
||||
<Typography variant="overline">
|
||||
<Skeleton width={80} />
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Skeleton width={180} />
|
||||
</Typography>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
minHeight: (theme) =>
|
||||
(theme.typography.body2.lineHeight as number) * 2 + "em",
|
||||
}}
|
||||
>
|
||||
<Skeleton width={120} />
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions sx={{ mb: 1, mx: 1 }}>
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={20}
|
||||
variant="rectangular"
|
||||
sx={{ borderRadius: 1, mr: "auto" }}
|
||||
/>
|
||||
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
48
src/components/Home/TableGrid/TableGridSkeleton.tsx
Normal file
48
src/components/Home/TableGrid/TableGridSkeleton.tsx
Normal file
@@ -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 (
|
||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
height: 48,
|
||||
maxWidth: (theme) => theme.breakpoints.values.sm - 48,
|
||||
width: { xs: "100%", md: "50%", lg: "100%" },
|
||||
mx: "auto",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCardSkeleton />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCardSkeleton />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCardSkeleton />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCardSkeleton />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCardSkeleton />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCardSkeleton />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
73
src/components/Home/TableGrid/index.tsx
Normal file
73
src/components/Home/TableGrid/index.tsx
Normal file
@@ -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<string, Table[]>;
|
||||
getLink: (table: Table) => string;
|
||||
getActions?: (table: Table) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableGrid({
|
||||
sections,
|
||||
getLink,
|
||||
getActions,
|
||||
}: ITableGridProps) {
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{Object.entries(sections).map(
|
||||
([sectionName, sectionTables], sectionIndex) => {
|
||||
const tableItems = sectionTables
|
||||
.map((table, tableIndex) => {
|
||||
if (!table) return null;
|
||||
|
||||
return (
|
||||
<SlideTransition
|
||||
key={table.id}
|
||||
appear
|
||||
timeout={(sectionIndex + 1) * 100 + tableIndex * 50}
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCard
|
||||
{...table}
|
||||
link={getLink(table)}
|
||||
actions={getActions ? getActions(table) : null}
|
||||
/>
|
||||
</Grid>
|
||||
</SlideTransition>
|
||||
);
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
if (tableItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={sectionName}>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SlideTransition
|
||||
key={"grid-section-" + sectionName}
|
||||
in
|
||||
timeout={(sectionIndex + 1) * 100}
|
||||
>
|
||||
<SectionHeading sx={{ pl: 2, pr: 1.5 }}>
|
||||
{sectionName}
|
||||
</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
<Grid component={TransitionGroup} container spacing={2}>
|
||||
{tableItems}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
78
src/components/Home/TableList/TableListItem.tsx
Normal file
78
src/components/Home/TableList/TableListItem.tsx
Normal file
@@ -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 (
|
||||
<ListItem disableGutters disablePadding>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{
|
||||
alignItems: "baseline",
|
||||
height: 48,
|
||||
py: 0,
|
||||
pr: 0,
|
||||
borderRadius: 2,
|
||||
"& > *": { lineHeight: "48px !important" },
|
||||
flexWrap: "nowrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* <Typography
|
||||
variant="overline"
|
||||
component="p"
|
||||
noWrap
|
||||
color="textSecondary"
|
||||
sx={{ maxWidth: 100, flexShrink: 0, flexGrow: 1, mr: 2 }}
|
||||
>
|
||||
{section}
|
||||
</Typography> */}
|
||||
<Typography
|
||||
component="h3"
|
||||
variant="button"
|
||||
noWrap
|
||||
sx={{ maxWidth: 160, flexShrink: 0, flexGrow: 1, mr: 2 }}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography color="textSecondary" noWrap>
|
||||
{description}
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
{actions}
|
||||
|
||||
<IconButton
|
||||
size="large"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{ display: { xs: "none", sm: "inline-flex" } }}
|
||||
>
|
||||
<GoIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
23
src/components/Home/TableList/TableListItemSkeleton.tsx
Normal file
23
src/components/Home/TableList/TableListItemSkeleton.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ListItem, Skeleton } from "@material-ui/core";
|
||||
|
||||
export default function TableListItemSkeleton() {
|
||||
return (
|
||||
<ListItem disableGutters disablePadding style={{ height: 48 }}>
|
||||
<Skeleton width={160} sx={{ mx: 2, flexShrink: 0 }} />
|
||||
<Skeleton sx={{ mr: 2, flexBasis: 240, flexShrink: 1 }} />
|
||||
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{ ml: "auto", mr: 3, flexShrink: 0 }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{ mr: 1.5, flexShrink: 0 }}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
36
src/components/Home/TableList/TableListSkeleton.tsx
Normal file
36
src/components/Home/TableList/TableListSkeleton.tsx
Normal file
@@ -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 (
|
||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
height: 48,
|
||||
maxWidth: (theme) => theme.breakpoints.values.sm - 48,
|
||||
width: { xs: "100%", md: "50%", lg: "100%" },
|
||||
mx: "auto",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
||||
<Paper>
|
||||
<TableListItemSkeleton />
|
||||
<TableListItemSkeleton />
|
||||
<TableListItemSkeleton />
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SectionHeadingSkeleton sx={{ pl: 2, pr: 1 }} />
|
||||
<Paper>
|
||||
<TableListItemSkeleton />
|
||||
<TableListItemSkeleton />
|
||||
<TableListItemSkeleton />
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
71
src/components/Home/TableList/index.tsx
Normal file
71
src/components/Home/TableList/index.tsx
Normal file
@@ -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<string, Table[]>;
|
||||
getLink: (table: Table) => string;
|
||||
getActions?: (table: Table) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableList({
|
||||
sections,
|
||||
getLink,
|
||||
getActions,
|
||||
}: ITableListProps) {
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{Object.entries(sections).map(
|
||||
([sectionName, sectionTables], sectionIndex) => {
|
||||
const tableItems = sectionTables
|
||||
.map((table) => {
|
||||
if (!table) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={table.id}>
|
||||
<TableListItem
|
||||
{...table}
|
||||
link={getLink(table)}
|
||||
actions={getActions ? getActions(table) : null}
|
||||
/>
|
||||
</Collapse>
|
||||
);
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
if (tableItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={sectionName}>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SlideTransition
|
||||
key={"list-section-" + sectionName}
|
||||
in
|
||||
timeout={(sectionIndex + 1) * 100}
|
||||
>
|
||||
<SectionHeading sx={{ pl: 2, pr: 1 }}>
|
||||
{sectionName}
|
||||
</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
<SlideTransition in timeout={(sectionIndex + 1) * 100}>
|
||||
<Paper>
|
||||
<List disablePadding>
|
||||
<TransitionGroup>{tableItems}</TransitionGroup>
|
||||
</List>
|
||||
</Paper>
|
||||
</SlideTransition>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
@@ -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)",
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function NavItem(props: MenuItemProps<Link>) {
|
||||
selected={pathname === props.to}
|
||||
{...props}
|
||||
sx={{
|
||||
...props.sx,
|
||||
"&&::before": {
|
||||
left: "auto",
|
||||
right: 0,
|
||||
|
||||
@@ -54,8 +54,12 @@ export default function NavDrawerItem({
|
||||
)}`
|
||||
}
|
||||
onClick={closeDrawer}
|
||||
sx={{
|
||||
ml: 2,
|
||||
width: (theme) => `calc(100% - ${theme.spacing(2 + 0.5)})`,
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={table.name} sx={{ pl: 2 }} />
|
||||
<ListItemText primary={table.name} />
|
||||
</NavItem>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
setThemeOverridden,
|
||||
} = useAppContext();
|
||||
if (!currentUser || !userDoc || !userDoc?.state?.doc)
|
||||
return <div style={{ width: 48, height: 48 }} />;
|
||||
return <div style={{ width: 48 - 12, height: 48 }} />;
|
||||
|
||||
const displayName = userDoc?.state?.doc?.user?.displayName;
|
||||
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
70
src/components/SectionHeading.tsx
Normal file
70
src/components/SectionHeading.tsx
Normal file
@@ -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<StackProps, "children"> {
|
||||
children: string;
|
||||
}
|
||||
|
||||
export const SectionHeading = forwardRef(function SectionHeading_(
|
||||
{ children, sx, ...props }: ISectionHeadingProps,
|
||||
ref
|
||||
) {
|
||||
const sectionLink = _camelCase(children);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={ref}
|
||||
direction="row"
|
||||
alignItems="flex-end"
|
||||
id={sectionLink}
|
||||
{...props}
|
||||
sx={{
|
||||
pb: 0.5,
|
||||
cursor: "default",
|
||||
...sx,
|
||||
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
"&:hover .sectionHeadingLink, &:active .sectionHeadingLink": {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
scrollMarginTop: (theme) => theme.spacing(APP_BAR_HEIGHT / 8 + 3.5),
|
||||
scrollBehavior: "smooth",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" component="h2">
|
||||
{children}
|
||||
</Typography>
|
||||
<IconButton
|
||||
component={HashLink}
|
||||
to={`#${sectionLink}`}
|
||||
smooth
|
||||
size="small"
|
||||
className="sectionHeadingLink"
|
||||
sx={{
|
||||
my: -0.5,
|
||||
ml: 1,
|
||||
|
||||
opacity: 0,
|
||||
transition: (theme) =>
|
||||
theme.transitions.create("opacity", {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
|
||||
"&:focus": { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
export default SectionHeading;
|
||||
14
src/components/SectionHeadingSkeleton.tsx
Normal file
14
src/components/SectionHeadingSkeleton.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Stack, StackProps, Skeleton } from "@material-ui/core";
|
||||
|
||||
export default function SectionHeadingSkeleton({ sx, ...props }: StackProps) {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-end"
|
||||
{...props}
|
||||
sx={{ pb: 0.5, ...sx }}
|
||||
>
|
||||
<Skeleton width={120} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<section style={{ cursor: "default" }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="h2"
|
||||
sx={{ mx: 1, mb: 0.5 }}
|
||||
id={title}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: { xs: 2, sm: 3 },
|
||||
<SlideTransition in timeout={transitionTimeout}>
|
||||
<SectionHeading sx={{ mx: 1 }}>{title}</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
"& > :not(style) + :not(style)": {
|
||||
m: 0,
|
||||
mt: { xs: 2, sm: 3 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
<SlideTransition in timeout={transitionTimeout + 50}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: { xs: 2, sm: 3 },
|
||||
|
||||
"& > :not(style) + :not(style)": {
|
||||
m: 0,
|
||||
mt: { xs: 2, sm: 3 },
|
||||
},
|
||||
|
||||
...paperSx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
</SlideTransition>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
81
src/components/Settings/UserManagement/InviteUser.tsx
Normal file
81
src/components/Settings/UserManagement/InviteUser.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Tooltip title="Invite User">
|
||||
<Zoom in>
|
||||
<Fab
|
||||
aria-label="Invite User"
|
||||
onClick={() => 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),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
</Tooltip>
|
||||
|
||||
{open && (
|
||||
<Modal
|
||||
title="Invite User"
|
||||
onClose={() => setOpen(false)}
|
||||
maxWidth="xs"
|
||||
body={
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Send an email to this user to invite them to join your project.
|
||||
</DialogContentText>
|
||||
<DialogContentText paragraph>
|
||||
They can sign up with any of the sign-in options{" "}
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={routes.projectSettings + "#authentication"}
|
||||
>
|
||||
you have enabled
|
||||
</MuiLink>
|
||||
, as long as they use the same email address.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
label="Email Address"
|
||||
id="invite-email"
|
||||
fullWidth
|
||||
autoFocus
|
||||
placeholder="name@example.com"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
actions={{ primary: { children: "Invite" } }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -26,12 +26,9 @@ export default function UserItem({
|
||||
<ListItemText
|
||||
primary={displayName}
|
||||
secondary={email}
|
||||
primaryTypographyProps={{
|
||||
style: { userSelect: "all" },
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
noWrap: true,
|
||||
style: { userSelect: "all" },
|
||||
sx={{
|
||||
overflowX: "hidden",
|
||||
"& > *": { 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,
|
||||
|
||||
@@ -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<ButtonProps>;
|
||||
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 (
|
||||
<Card className={clsx(className, classes.root)}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
className={classes.container}
|
||||
>
|
||||
<Grid item xs>
|
||||
<CardContent className={clsx(classes.container, classes.cardContent)}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
className={classes.container}
|
||||
>
|
||||
<Grid item>
|
||||
<Grid container className={classes.headerSection} spacing={3}>
|
||||
<Grid item xs>
|
||||
{overline && (
|
||||
<Typography
|
||||
variant="overline"
|
||||
className={classes.overline}
|
||||
>
|
||||
{overline}
|
||||
</Typography>
|
||||
)}
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{title && (
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{headerAction && headerAction}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{imageSource && (
|
||||
<Grid item>
|
||||
<CardMedia
|
||||
className={classes.image}
|
||||
image={imageSource}
|
||||
title={title}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs>
|
||||
{bodyContent && Array.isArray(bodyContent) ? (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{bodyContent.map((element) => (
|
||||
<Grid item>{element}</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{bodyContent}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Divider className={classes.divider} />
|
||||
<CardActions className={classes.cardActions}>
|
||||
{primaryButton && (
|
||||
<Button color="primary" variant="text" {...primaryButton} />
|
||||
)}
|
||||
{primaryLink && (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="text"
|
||||
component={Link as any}
|
||||
to={primaryLink.to}
|
||||
children={primaryLink.children || primaryLink.label}
|
||||
endIcon={<GoIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{secondaryAction}
|
||||
</CardActions>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export const AppProvider: React.FC = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${projectId} | ${name}`;
|
||||
document.title = `${projectId} • ${name}`;
|
||||
}, []);
|
||||
|
||||
// Store matching userDoc
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ImportWizardRef } from "components/Wizards/ImportWizard";
|
||||
import _find from "lodash/find";
|
||||
import { deepen } from "utils/fns";
|
||||
export type Table = {
|
||||
id: string;
|
||||
collection: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
@@ -111,7 +112,9 @@ export const RowyContextProvider: React.FC = ({ children }) => {
|
||||
|
||||
const _sections = _groupBy(filteredTables, "section");
|
||||
setSections(_sections);
|
||||
setTables(filteredTables);
|
||||
setTables(
|
||||
filteredTables.map((table) => ({ ...table, id: table.collection }))
|
||||
);
|
||||
}
|
||||
}, [settings, userRoles, sections]);
|
||||
|
||||
|
||||
17
src/hooks/useBasicSearch.ts
Normal file
17
src/hooks/useBasicSearch.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export default function useBasicSearch<T>(
|
||||
collection: T[],
|
||||
predicate: (item: T, query: string) => boolean,
|
||||
debounce: number = 400
|
||||
) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [handleQuery] = useDebouncedCallback(setQuery, debounce);
|
||||
|
||||
const results = query
|
||||
? collection.filter((user) => predicate(user, query.toLowerCase()))
|
||||
: collection;
|
||||
|
||||
return [results, query, handleQuery] as const;
|
||||
}
|
||||
@@ -1,104 +1,75 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useState, ChangeEvent } from "react";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
import _find from "lodash/find";
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Stack,
|
||||
Typography,
|
||||
Divider,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Tooltip,
|
||||
Fab,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Link as MuiLink,
|
||||
Button,
|
||||
Zoom,
|
||||
} from "@material-ui/core";
|
||||
import ViewListIcon from "@material-ui/icons/ViewListOutlined";
|
||||
import ViewGridIcon from "@material-ui/icons/ViewModuleOutlined";
|
||||
import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder";
|
||||
import FavoriteIcon from "@material-ui/icons/Favorite";
|
||||
import EditIcon from "@material-ui/icons/EditOutlined";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import Favorite from "@material-ui/icons/Favorite";
|
||||
import FavoriteBorder from "@material-ui/icons/FavoriteBorder";
|
||||
import SecurityIcon from "@material-ui/icons/SecurityOutlined";
|
||||
|
||||
import StyledCard from "components/StyledCard";
|
||||
import HomeWelcomePrompt from "components/HomeWelcomePrompt";
|
||||
import EmptyState from "components/EmptyState";
|
||||
import FloatingSearch from "components/FloatingSearch";
|
||||
import TableGrid from "components/Home/TableGrid";
|
||||
import TableList from "components/Home/TableList";
|
||||
import TableGridSkeleton from "components/Home/TableGrid/TableGridSkeleton";
|
||||
import TableListSkeleton from "components/Home/TableList/TableListSkeleton";
|
||||
import HomeWelcomePrompt from "components/Home/HomeWelcomePrompt";
|
||||
import AccessDenied from "components/Home/AccessDenied";
|
||||
|
||||
import routes from "constants/routes";
|
||||
import { useRowyContext } from "contexts/RowyContext";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useRowyContext, Table } from "contexts/RowyContext";
|
||||
import useDoc, { DocActions } from "hooks/useDoc";
|
||||
import useBasicSearch from "hooks/useBasicSearch";
|
||||
import TableSettingsDialog, {
|
||||
TableSettingsDialogModes,
|
||||
} from "components/TableSettings";
|
||||
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
import { SETTINGS } from "config/dbPaths";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
"@global": {
|
||||
html: { scrollBehavior: "smooth" },
|
||||
},
|
||||
|
||||
root: {
|
||||
minHeight: "100vh",
|
||||
paddingBottom: theme.spacing(8),
|
||||
},
|
||||
|
||||
section: {
|
||||
paddingTop: theme.spacing(10),
|
||||
"&:first-of-type": { marginTop: theme.spacing(2) },
|
||||
},
|
||||
sectionHeader: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
divider: { margin: theme.spacing(1, 0, 3) },
|
||||
|
||||
cardGrid: {
|
||||
[theme.breakpoints.down("sm")]: { maxWidth: 360, margin: "0 auto" },
|
||||
},
|
||||
card: {
|
||||
height: "100%",
|
||||
[theme.breakpoints.up("md")]: { minHeight: 220 },
|
||||
[theme.breakpoints.down("lg")]: { minHeight: 180 },
|
||||
},
|
||||
favButton: {
|
||||
margin: theme.spacing(-0.5, -1, 0, 0),
|
||||
},
|
||||
configFab: {
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(3),
|
||||
right: theme.spacing(12),
|
||||
},
|
||||
fab: {
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(3),
|
||||
right: theme.spacing(3),
|
||||
},
|
||||
})
|
||||
);
|
||||
const useHomeViewState = createPersistedState("__ROWY__HOME_VIEW");
|
||||
|
||||
export default function HomePage() {
|
||||
const classes = useStyles();
|
||||
const { userDoc } = useAppContext();
|
||||
const { tables, userClaims } = useRowyContext();
|
||||
|
||||
const [results, query, handleQuery] = useBasicSearch(
|
||||
tables ?? [],
|
||||
(table, query) =>
|
||||
table.id.toLowerCase().includes(query) ||
|
||||
table.name.toLowerCase().includes(query) ||
|
||||
table.section.toLowerCase().includes(query) ||
|
||||
table.description.toLowerCase().includes(query)
|
||||
);
|
||||
|
||||
const [view, setView] = useHomeViewState("grid");
|
||||
|
||||
const favorites = Array.isArray(userDoc.state.doc?.favoriteTables)
|
||||
? userDoc.state.doc.favoriteTables
|
||||
: [];
|
||||
const sections = {
|
||||
Favorites: favorites.map((id) => _find(results, { id })),
|
||||
..._groupBy(results, "section"),
|
||||
};
|
||||
|
||||
const [settingsDialogState, setSettingsDialogState] = useState<{
|
||||
mode: null | TableSettingsDialogModes;
|
||||
data: null | {
|
||||
collection: string;
|
||||
description: string;
|
||||
roles: string[];
|
||||
name: string;
|
||||
section: string;
|
||||
isCollectionGroup: boolean;
|
||||
tableType: string;
|
||||
};
|
||||
}>({
|
||||
mode: null,
|
||||
data: null,
|
||||
});
|
||||
data: null | (Table & { tableType: string });
|
||||
}>({ mode: null, data: null });
|
||||
|
||||
const clearDialog = () =>
|
||||
setSettingsDialogState({
|
||||
@@ -106,228 +77,170 @@ export default function HomePage() {
|
||||
data: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const modal = decodeURIComponent(
|
||||
queryString.parse(window.location.search).modal as string
|
||||
);
|
||||
if (modal) {
|
||||
switch (modal) {
|
||||
case "settings":
|
||||
setOpenProjectSettings(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [window.location.search]);
|
||||
const { sections } = useRowyContext();
|
||||
const { userDoc } = useAppContext();
|
||||
|
||||
const favs = userDoc.state.doc?.favoriteTables
|
||||
? userDoc.state.doc.favoriteTables
|
||||
: [];
|
||||
|
||||
const handleCreateTable = () =>
|
||||
setSettingsDialogState({
|
||||
mode: TableSettingsDialogModes.create,
|
||||
data: null,
|
||||
});
|
||||
const [openProjectSettings, setOpenProjectSettings] = useState(false);
|
||||
const [openBuilderInstaller, setOpenBuilderInstaller] = useState(false);
|
||||
|
||||
const [settingsDocState, settingsDocDispatch] = useDoc({ path: SETTINGS });
|
||||
useEffect(() => {
|
||||
if (!settingsDocState.loading && !settingsDocState.doc) {
|
||||
settingsDocDispatch({
|
||||
action: DocActions.update,
|
||||
data: { createdAt: new Date() },
|
||||
});
|
||||
}
|
||||
}, [settingsDocState]);
|
||||
if (settingsDocState.error?.code === "permission-denied") {
|
||||
return (
|
||||
<EmptyState
|
||||
fullScreen
|
||||
Icon={SecurityIcon}
|
||||
message="Access Denied"
|
||||
description={
|
||||
<>
|
||||
<Typography>
|
||||
You do not have access to this project. Please contact the project
|
||||
owner.
|
||||
</Typography>
|
||||
<Typography>
|
||||
If you are the project owner, please follow{" "}
|
||||
<MuiLink
|
||||
href={WIKI_LINKS.securityRules}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
these instructions
|
||||
</MuiLink>{" "}
|
||||
to set up the project’s security rules.
|
||||
</Typography>
|
||||
const [settingsDocState] = useDoc(
|
||||
{ path: SETTINGS },
|
||||
{ createIfMissing: true }
|
||||
);
|
||||
|
||||
<Button component={Link} to={routes.signOut}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
bgcolor: "background.default",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(tables))
|
||||
return view === "list" ? <TableListSkeleton /> : <TableGridSkeleton />;
|
||||
|
||||
const TableCard = ({ table }) => {
|
||||
const checked = Boolean(_find(favs, table));
|
||||
return (
|
||||
<Grid key={table.name} item xs={12} sm={6} md={4} lg={4} xl={3}>
|
||||
<StyledCard
|
||||
className={classes.card}
|
||||
overline={table.section}
|
||||
title={table.name}
|
||||
headerAction={
|
||||
<Checkbox
|
||||
onClick={() => {
|
||||
userDoc.dispatch({
|
||||
action: DocActions.update,
|
||||
data: {
|
||||
favoriteTables: checked
|
||||
? favs.filter((t) => t.collection !== table.collection)
|
||||
: [...favs, table],
|
||||
},
|
||||
});
|
||||
}}
|
||||
checked={checked}
|
||||
icon={<FavoriteBorder />}
|
||||
checkedIcon={<Favorite />}
|
||||
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 <AccessDenied />;
|
||||
|
||||
const createTableFab = (
|
||||
<Tooltip title="Create Table">
|
||||
<Zoom in>
|
||||
<Fab
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleCreateTable}
|
||||
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) }),
|
||||
}}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setSettingsDialogState({
|
||||
mode: TableSettingsDialogModes.update,
|
||||
data: table,
|
||||
})
|
||||
}
|
||||
aria-label="Edit table"
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (tables.length === 0 && userClaims.roles.includes("ADMIN"))
|
||||
return (
|
||||
<>
|
||||
<HomeWelcomePrompt />
|
||||
{createTableFab}
|
||||
</>
|
||||
);
|
||||
|
||||
const getLink = (table: Table) =>
|
||||
`${
|
||||
table.isCollectionGroup ? routes.tableGroup : routes.table
|
||||
}/${table.id.replace(/\//g, "~2F")}`;
|
||||
|
||||
const handleFavorite = (id: string) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||
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") && (
|
||||
<IconButton
|
||||
aria-label="Edit table"
|
||||
onClick={() =>
|
||||
setSettingsDialogState({
|
||||
mode: TableSettingsDialogModes.update,
|
||||
data: table as any,
|
||||
})
|
||||
}
|
||||
size={view === "list" ? "large" : undefined}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Checkbox
|
||||
onChange={handleFavorite(table.id)}
|
||||
checked={favorites.includes(table.id)}
|
||||
icon={<FavoriteBorderIcon />}
|
||||
checkedIcon={
|
||||
<Zoom in>
|
||||
<FavoriteIcon />
|
||||
</Zoom>
|
||||
}
|
||||
name={`favorite-${table.id}`}
|
||||
inputProps={{ "aria-label": "Favorite" }}
|
||||
sx={view === "list" ? { p: 1.5 } : undefined}
|
||||
color="secondary"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<main className={classes.root}>
|
||||
{sections && Object.keys(sections).length > 0 ? (
|
||||
<Container>
|
||||
{favs.length !== 0 && (
|
||||
<section id="favorites" className={classes.section}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
className={classes.sectionHeader}
|
||||
>
|
||||
Favorites
|
||||
</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{favs.map((table) => (
|
||||
<TableCard key={table.collection} table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</section>
|
||||
)}
|
||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<FloatingSearch
|
||||
label="Search Tables"
|
||||
onChange={(e) => 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) => (
|
||||
<section
|
||||
key={sectionName}
|
||||
id={sectionName}
|
||||
className={classes.section}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
className={classes.sectionHeader}
|
||||
>
|
||||
{sectionName === "undefined" ? "Other" : sectionName}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
sx={{ pl: 2, cursor: "default" }}
|
||||
>
|
||||
{query ? `${results.length} of ${tables.length}` : tables.length}{" "}
|
||||
Tables
|
||||
</Typography>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
<ToggleButtonGroup
|
||||
value={view}
|
||||
exclusive
|
||||
onChange={(_, v) => {
|
||||
if (v !== null) setView(v);
|
||||
}}
|
||||
aria-label="Table view"
|
||||
sx={{ "& .MuiToggleButton-root": { borderRadius: 2 } }}
|
||||
>
|
||||
<ToggleButton value="list" aria-label="List view">
|
||||
<ViewListIcon style={{ transform: "rotate(180deg)" }} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="grid" aria-label="Grid view">
|
||||
<ViewGridIcon />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{sections[sectionName].map((table, i) => (
|
||||
<TableCard key={`${i}-${table.collection}`} table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className={classes.section}>
|
||||
<Tooltip title="Create Table">
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleCreateTable}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
</section>
|
||||
</Container>
|
||||
{view === "list" ? (
|
||||
<TableList
|
||||
sections={sections}
|
||||
getLink={getLink}
|
||||
getActions={getActions}
|
||||
/>
|
||||
) : (
|
||||
<Container>
|
||||
<HomeWelcomePrompt />
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleCreateTable}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Container>
|
||||
<TableGrid
|
||||
sections={sections}
|
||||
getLink={getLink}
|
||||
getActions={getActions}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TableSettingsDialog
|
||||
clearDialog={clearDialog}
|
||||
mode={settingsDialogState.mode}
|
||||
data={settingsDialogState.data}
|
||||
/>
|
||||
</main>
|
||||
{userClaims.roles.includes("ADMIN") && (
|
||||
<>
|
||||
{createTableFab}
|
||||
<TableSettingsDialog
|
||||
clearDialog={clearDialog}
|
||||
mode={settingsDialogState.mode}
|
||||
data={settingsDialogState.data}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function ProjectSettingsPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
{settingsState.loading || publicSettingsState.loading ? (
|
||||
<Stack spacing={4}>
|
||||
<SettingsSkeleton />
|
||||
@@ -78,15 +78,15 @@ export default function ProjectSettingsPage() {
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing={4}>
|
||||
<SettingsSection title="About">
|
||||
<SettingsSection title="About" transitionTimeout={100}>
|
||||
<About />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={`${name} Run`}>
|
||||
<SettingsSection title={`${name} Run`} transitionTimeout={200}>
|
||||
<CloudRun {...childProps} />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Authentication">
|
||||
<SettingsSection title="Authentication" transitionTimeout={300}>
|
||||
<Authentication {...childProps} />
|
||||
</SettingsSection>
|
||||
</Stack>
|
||||
|
||||
@@ -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<User>(
|
||||
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 (
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<FloatingSearch label="Search Users" onChange={handleChange as any} />
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<FloatingSearch
|
||||
label="Search Users"
|
||||
onChange={(e) => handleQuery(e.target.value)}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
alignItems="baseline"
|
||||
sx={{ mt: 4, mx: 1, mb: 0.5, cursor: "default" }}
|
||||
>
|
||||
<Typography variant="subtitle1" component="h2">
|
||||
Users
|
||||
</Typography>
|
||||
{!loading && (
|
||||
<Typography variant="button" component="div">
|
||||
{query
|
||||
? `${filteredUsers.length} of ${usersState.documents.length}`
|
||||
: usersState.documents.length}
|
||||
<SlideTransition in timeout={100}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
alignItems="baseline"
|
||||
sx={{ mt: 4, mx: 1, mb: 0.5, cursor: "default" }}
|
||||
>
|
||||
<Typography variant="subtitle1" component="h2">
|
||||
Users
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Paper>
|
||||
<List>
|
||||
{loading || (query === "" && filteredUsers.length === 0) ? (
|
||||
<>
|
||||
<UserSkeleton />
|
||||
<UserSkeleton />
|
||||
<UserSkeleton />
|
||||
</>
|
||||
) : (
|
||||
filteredUsers.map((user) => <UserItem key={user.id} {...user} />)
|
||||
{!loading && (
|
||||
<Typography variant="button" component="div">
|
||||
{query
|
||||
? `${results.length} of ${usersState.documents.length}`
|
||||
: usersState.documents.length}
|
||||
</Typography>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</SlideTransition>
|
||||
|
||||
{loading || (query === "" && results.length === 0) ? (
|
||||
<Paper>
|
||||
<List>
|
||||
<UserSkeleton />
|
||||
<UserSkeleton />
|
||||
<UserSkeleton />
|
||||
</List>
|
||||
</Paper>
|
||||
) : (
|
||||
<SlideTransition in timeout={100 + 50}>
|
||||
<Paper>
|
||||
<List>
|
||||
{results.map((user) => (
|
||||
<UserItem key={user.id} {...user} />
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</SlideTransition>
|
||||
)}
|
||||
|
||||
<InviteUser />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import SettingsSection from "components/Settings/SettingsSection";
|
||||
|
||||
export default function UserSettingsPage() {
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<Stack spacing={4}>
|
||||
<SettingsSection title="Your Account">TODO:</SettingsSection>
|
||||
</Stack>
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
|
||||
@@ -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: <RadioIcon />,
|
||||
checkedIcon: <RadioIcon />,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: { padding: theme.spacing(1) },
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
defaultProps: {
|
||||
@@ -603,6 +616,9 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
checkedIcon: <CheckboxIcon />,
|
||||
indeterminateIcon: <CheckboxIndeterminateIcon />,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: { padding: theme.spacing(1) },
|
||||
},
|
||||
},
|
||||
|
||||
MuiSlider: {
|
||||
|
||||
24
yarn.lock
24
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"
|
||||
|
||||
Reference in New Issue
Block a user