Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add layout for primary/detail view #233

Merged
merged 54 commits into from
Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
328c0fe
feat: add useMediaQuery hook
mikemurray Feb 29, 2020
f5fba42
feat: add useOperatorRoutes hook
mikemurray Feb 29, 2020
28f6001
feat: add new primary/detail layout
mikemurray Feb 29, 2020
28ac4d2
feat: remove left border
mikemurray Feb 29, 2020
b5ede51
feat: add component for rendering operator routes
mikemurray Feb 29, 2020
a5f53d3
feat: add state management callbacks
mikemurray Feb 29, 2020
24fe37e
feat: update prop names and add warnings
mikemurray Feb 29, 2020
7180729
refactor: convert to function component and use hooks
mikemurray Feb 29, 2020
0cb5d2b
refactor: update route registration options
mikemurray Feb 29, 2020
987b1d1
feat: use primary/detail layout component
mikemurray Feb 29, 2020
d213b0c
fix: use component for check
mikemurray Mar 2, 2020
2b1d905
refactor: use `MainComponent`
mikemurray Mar 2, 2020
abe137a
refactor: use updated configuration
mikemurray Mar 2, 2020
b89d8f2
refactor: use updated configuration
mikemurray Mar 2, 2020
3a53dd7
refactor: use updated configuration
mikemurray Mar 2, 2020
33638d3
refactor: use updated configuration
mikemurray Mar 2, 2020
105d511
refactor: use updated configuration
mikemurray Mar 2, 2020
b8ce48e
refactor: use updated configuration
mikemurray Mar 2, 2020
64768e8
refactor: use updated configuration
mikemurray Mar 2, 2020
69255d1
refactor: use updated configuration
mikemurray Mar 2, 2020
054a958
refactor: use updated configuration
mikemurray Mar 2, 2020
106043b
refactor: use updated configuration
mikemurray Mar 2, 2020
78a6e1a
refactor: use updated configuration
mikemurray Mar 2, 2020
32d4ae9
refactor: use updated configuration
mikemurray Mar 2, 2020
c094011
refactor: use updated configuration
mikemurray Mar 2, 2020
624016f
refactor: use updated configuration
mikemurray Mar 2, 2020
eddea2e
refactor: use `useOperatorRoutes` hook to get routes
mikemurray Mar 2, 2020
f9cd77b
fix: component resolution
mikemurray Mar 2, 2020
05311e2
refactor: use `useOperatorRoutes` hook
mikemurray Mar 2, 2020
18fa21b
fix: filtering group routes
mikemurray Mar 2, 2020
0630d71
refactor: fix spacing
mikemurray Mar 2, 2020
df8c557
refactor: use `useOperatorRoutes` hook
mikemurray Mar 2, 2020
1a18b26
fix: remove unused operator routes
mikemurray Mar 2, 2020
4f96816
fix: remove unused routes prop
mikemurray Mar 2, 2020
eb228cb
feat: use a more descriptive name for the main nav group
mikemurray Mar 3, 2020
6c88064
fix: use navigation group
mikemurray Mar 3, 2020
94cd6fe
fix: use navigation group
mikemurray Mar 3, 2020
8f3caff
fix: use navigation group
mikemurray Mar 3, 2020
55ad242
fix: use navigation group
mikemurray Mar 3, 2020
e9dc96e
fix: use navigation group
mikemurray Mar 3, 2020
6363a40
fix: use navigation group
mikemurray Mar 3, 2020
322c36e
fix: force primary nav open on desktop
mikemurray Mar 3, 2020
430bf16
feat: allow for null templates and exact matched routes
mikemurray Mar 3, 2020
a2f006e
fix: make routes exact match
mikemurray Mar 3, 2020
d888acb
fix: use navigation group
mikemurray Mar 3, 2020
5ecafa9
fix: use no layout
mikemurray Mar 3, 2020
a6260ed
feat: drawer and layout updates
mikemurray Mar 3, 2020
68aa4fe
feat: use full width on mobile
mikemurray Mar 3, 2020
fa12120
fix: update to smallest breakpoint
mikemurray Mar 3, 2020
c55cd76
fix: remove prop requirement and use default props
mikemurray Mar 3, 2020
b4dc83b
fix: remove invalid prop
mikemurray Mar 3, 2020
3dfb81e
fix: move `disableElevation` prop to correct element
mikemurray Mar 3, 2020
d5c066e
fix: remove uiState props. UI state can be found retrieved from the U…
mikemurray Mar 3, 2020
e144dac
fix: remove unused prop definition
mikemurray Mar 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions imports/client/ui/components/Sidebar/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ import { Translation } from "/imports/plugins/core/ui/client/components";
import useIsAppLoading from "/imports/client/ui/hooks/useIsAppLoading.js";
import useCurrentShopId from "../../hooks/useCurrentShopId";
import ShopLogoWithData from "../ShopLogoWithData";
import useOperatorRoutes from "../../hooks/useOperatorRoutes";

const activeClassName = "nav-item-active";

// Route sorting by priority. Items without a priority get pushed the bottom.
const routeSort = (routeA, routeB) => (routeA.priority || Number.MAX_SAFE_INTEGER) - (routeB.priority || Number.MAX_SAFE_INTEGER);

const styles = (theme) => ({
closeButton: {
"color": theme.palette.colors.white,
Expand Down Expand Up @@ -104,15 +102,13 @@ function Sidebar(props) {
isSidebarOpen,
onDrawerClose,
isSettingsOpen,
setIsSettingsOpen,
routes
setIsSettingsOpen
} = props;

const [isAppLoading] = useIsAppLoading();
const [currentShopId] = useCurrentShopId();

const primaryRoutes = routes.filter(({ isNavigationLink, isSetting }) => isNavigationLink && !isSetting).sort(routeSort);
const settingRoutes = routes.filter(({ isNavigationLink, isSetting }) => isNavigationLink && isSetting).sort(routeSort);
const primaryRoutes = useOperatorRoutes({ groups: ["navigation"] });
const settingRoutes = useOperatorRoutes({ groups: ["settings"] });

let drawerProps = {
classes: {
Expand Down Expand Up @@ -245,7 +241,6 @@ Sidebar.propTypes = {
isSettingsOpen: PropTypes.bool,
isSidebarOpen: PropTypes.bool.isRequired,
onDrawerClose: PropTypes.func.isRequired,
routes: PropTypes.array,
setIsSettingsOpen: PropTypes.func.isRequired
};

Expand Down
4 changes: 3 additions & 1 deletion imports/client/ui/context/UIContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export const UIContext = createContext({
onCloseDetailDrawer: () => { },
onClosePrimarySidebar: () => { },
onToggleDetailDrawer: () => { },
onTogglePrimarySidebar: () => { }
onTogglePrimarySidebar: () => { },
setDetailDrawerOpen: () => { },
setPrimarySidebarOpen: () => { }
});
20 changes: 20 additions & 0 deletions imports/client/ui/hooks/useMediaQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMediaQuery as useMediaQueryMui } from "@material-ui/core";

/**
* Media query hook. Wraps the mui hook `useMediaQuery` with some additional options.
* @param {String} breakpoint Breakpoint name. `mobile|tablet|desktop|xs|sm|md|lg`
* @param {Options} options Options
* @returns {Boolean} Whether the breakpoint is active
*/
export default function useMediaQuery(breakpoint = "mobile", options) {
return useMediaQueryMui((theme) => {
if (breakpoint === "mobile") {
return theme.breakpoints.down("sm", options);
} else if (breakpoint === "tablet") {
return theme.breakpoints.down("md", options);
} else if (breakpoint === "desktop") {
return theme.breakpoints.up("md", options);
}
return theme.breakpoints.up(breakpoint, options);
});
}
42 changes: 42 additions & 0 deletions imports/client/ui/hooks/useOperatorRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMemo } from "react";
import { operatorRoutes } from "../index";

export const defaultRouteSort = (routeA, routeB) => (
(routeA.priority || Number.MAX_SAFE_INTEGER) - (routeB.priority || Number.MAX_SAFE_INTEGER)
);

/**
* Operator routes hook
* @param {Object} options Options
* @param {Object} [options.LayoutComponent] LayoutComponent override
* @param {Object} [options.group] Filter routes by group name
* @param {Object} [options.filter] Custom filter
* @param {Object} [options.sort] Route sort function
* @returns {Array} An array containing filtered routes
*/
export default function useOperatorRoutes(options = {}) {
const {
groups,
filter,
sort = defaultRouteSort
} = options;

const routes = useMemo(() => {
let filteredRoutes;
if (Array.isArray(groups)) {
filteredRoutes = operatorRoutes.filter(({ group: routeGroup }) => groups.includes(routeGroup));
} else if (filter) {
filteredRoutes = operatorRoutes.filter(filter);
} else {
filteredRoutes = operatorRoutes;
}

if (sort) {
filteredRoutes = filteredRoutes.sort(sort);
}

return filteredRoutes;
}, [filter, groups, sort]);

return routes;
}
48 changes: 42 additions & 6 deletions imports/client/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import "./appComponents";

export const operatorRoutes = [];
export const routes = [];
export const defaultRouteGroups = {
navigation: "navigation",
settings: "settings"
};

/**
* @name registerOperatorRoute
Expand All @@ -19,16 +23,48 @@ export const routes = [];
* @returns {undefined}
*/
export function registerOperatorRoute(route) {
const { mainComponent, hocs = [] } = route;
let component = mainComponent;
const { isNavigationLink, isSetting, layoutComponent, mainComponent, MainComponent, hocs = [] } = route;
const additionalProps = {};

if (typeof mainComponent === "string") {
component = () => getReactComponentOrBlazeTemplate(mainComponent);
if (isNavigationLink) {
// eslint-disable-next-line no-console
console.warn("Option `isNavigationLink` is deprecated. Set `group: \"main\"` in your route configuration");
additionalProps.group = defaultRouteGroups.main;
}

if (isSetting) {
// eslint-disable-next-line no-console
console.warn("Option `isSetting` is deprecated. Set `group: \"settings\"` in your route configuration");
additionalProps.group = defaultRouteGroups.settings;
}

if (layoutComponent) {
// eslint-disable-next-line no-console
console.warn("Option `layoutComponent` is deprecated. Use `LayoutComponent` instead");
additionalProps.LayoutComponent = layoutComponent;
}

if (mainComponent) {
// eslint-disable-next-line no-console
console.warn("Option `mainComponent` is deprecated. Use `MainComponent` instead");
}

const resolvedMainComponent = MainComponent || mainComponent;
let component;

if (typeof resolvedMainComponent === "string") {
component = () => getReactComponentOrBlazeTemplate(resolvedMainComponent);
} else {
component = resolvedMainComponent;
}

component = compose(...hocs, setDisplayName(`Reaction(${name})`))(component);

operatorRoutes.push({ ...route, mainComponent: component });
operatorRoutes.push({
...route,
...additionalProps,
MainComponent: component
});
}

/**
Expand Down Expand Up @@ -56,6 +92,6 @@ export function registerRoute(route) {
routes.push({
exact: true,
...route,
mainComponent: component
MainComponent: component
});
}
178 changes: 178 additions & 0 deletions imports/client/ui/layouts/ContentViewPrimaryDetailLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* Component provides a regions for a primary (sidebar) and detail view
*/
import React, { useContext, useState } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Button from "@reactioncommerce/catalyst/Button";
import { Blocks } from "@reactioncommerce/reaction-components";
import {
AppBar,
Box,
Drawer,
Toolbar,
IconButton,
makeStyles
} from "@material-ui/core";
import ChevronDownIcon from "mdi-material-ui/ChevronDown";
import useMediaQuery from "../hooks/useMediaQuery";
import { UIContext } from "../context/UIContext";

const useStyles = makeStyles((theme) => ({
root: {
width: "100vw",
height: "100vh",
paddingTop: theme.mixins.toolbar.minHeight,
flexGrow: 1,
transition: "padding 225ms cubic-bezier(0, 0, 0.2, 1) 0ms",
overflow: "hidden",
[`${theme.breakpoints.up("xs")} and (orientation: landscape)`]: {
paddingTop: 54
},
[`${theme.breakpoints.up("xs")} and (orientation: portrait)`]: {
paddingTop: 54
},
[theme.breakpoints.up("sm")]: {
paddingTop: theme.mixins.toolbar.minHeight
}
},
block: {
marginBottom: theme.spacing(3)
},
drawerButton: {
borderRadius: 0
},
sidebar: {
flex: "1 1 auto",
maxWidth: 330,
height: `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`,
overflowY: "auto",
borderRight: `1px solid ${theme.palette.divider}`
},
content: {
flex: "1 1 auto",
height: `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`,
overflowY: "auto",
paddingTop: theme.spacing(5)
},
title: {
flex: 1
},
leadingDrawerOpen: {
paddingLeft: theme.dimensions.drawerWidth
},
trailingDrawerOpen: {
paddingRight: theme.dimensions.detailDrawerWidth
},
drawerPaperAnchorBottom: {
width: "100%",
height: "80%"
}
}));

/**
* Primary/Detail layout
* @param {Object} props ComponentProps
* @returns {React.ReactElement} A react element representing the primary/detail layout
*/
function ContentViewPrimaryDetailLayout(props) {
const [isDrawerOpen, setDrawerOpen] = useState(false);
const {
AppBarComponent,
DetailComponent,
PrimaryComponent,
children,
detailBlockRegionName,
drawerButtonTitle,
primaryBlockRegionName,
...blockProps
} = props;

const classes = useStyles();
const { isPrimarySidebarOpen, isDetailDrawerOpen } = useContext(UIContext);
const isMobile = useMediaQuery("mobile");

const closeDrawer = () => {
setDrawerOpen(false);
};

return (
<div
className={
classNames(classes.root, {
[classes.leadingDrawerOpen]: isPrimarySidebarOpen && !isMobile,
[classes.trailingDrawerOpen]: isDetailDrawerOpen && !isMobile
})
}
>
{AppBarComponent}
{isMobile &&
<>
<Button
color="default"
disableElevation={true}
className={classes.drawerButton}
fullWidth
onClick={() => setDrawerOpen(true)}
variant="contained"
>
<ChevronDownIcon /> {drawerButtonTitle}
</Button>

<Drawer
anchor="bottom"
classes={{
paperAnchorBottom: classes.drawerPaperAnchorBottom
}}
open={isDrawerOpen}
onClose={closeDrawer}
>
<AppBar
color="default"
elevation={0}
position="sticky"
>
<Toolbar>
<Box display="flex" justifyContent="center" width="100%">
<IconButton onClick={closeDrawer}>
<ChevronDownIcon />
</IconButton>
</Box>
</Toolbar>
</AppBar>
{PrimaryComponent || <Blocks region={primaryBlockRegionName} blockProps={blockProps} />}
</Drawer>
</>
}
<Box display="flex">
{!isMobile &&
<div className={classes.sidebar}>
{PrimaryComponent || <Blocks region={primaryBlockRegionName} blockProps={blockProps} />}
</div>
}

<div className={classes.content}>
{DetailComponent || <Blocks region={detailBlockRegionName} blockProps={blockProps} />}
</div>
</Box>
</div>
);
}

ContentViewPrimaryDetailLayout.propTypes = {
AppBarComponent: PropTypes.node,
DetailComponent: PropTypes.node,
DetailContainerProps: PropTypes.object,
PrimaryComponent: PropTypes.node,
children: PropTypes.node,
detailBlockRegionName: PropTypes.string,
drawerButtonTitle: PropTypes.string,
isMobile: PropTypes.bool,
primaryBlockRegionName: PropTypes.string
};

ContentViewPrimaryDetailLayout.defaultProps = {
drawerButtonTitle: "More"
};

export default ContentViewPrimaryDetailLayout;
Loading