Skip to content

Commit

Permalink
feat: In-page editor navigation menus (#3336)
Browse files Browse the repository at this point in the history
Co-authored-by: Dafydd Llŷr Pearson <[email protected]>
  • Loading branch information
ianjon3s and DafyddLlyr authored Jul 12, 2024
1 parent e1bc433 commit b484df1
Show file tree
Hide file tree
Showing 27 changed files with 529 additions and 832 deletions.
6 changes: 2 additions & 4 deletions e2e/tests/ui-driven/src/create-flow/create-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,7 @@ test.describe("Navigation", () => {
await page.goto(`/${context.team.slug}/${serviceProps.slug}`);

// Open flow settings
// TODO: Access via sidebar when EDITOR_NAVIGATION flag is removed
page.getByLabel("Toggle Menu").click();
page.getByText("Flow Settings").click();
page.locator('[aria-label="Service settings"]').click();

// Toggle flow online
page.getByLabel("Offline").click();
Expand All @@ -219,7 +217,7 @@ test.describe("Navigation", () => {
).toBeVisible();

// Exit back to main Editor page
page.getByRole("link", { name: "Close" }).click();
page.locator('[aria-label="Editor"]').click();

const previewLink = page.getByRole("link", {
name: "Open published service",
Expand Down
232 changes: 232 additions & 0 deletions editor.planx.uk/src/components/EditorNavMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
import FactCheckIcon from "@mui/icons-material/FactCheck";
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import GroupIcon from "@mui/icons-material/Group";
import PaletteIcon from "@mui/icons-material/Palette";
import RateReviewIcon from "@mui/icons-material/RateReview";
import TuneIcon from "@mui/icons-material/Tune";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import { styled } from "@mui/material/styles";
import Tooltip, { tooltipClasses, TooltipProps } from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { Role } from "@opensystemslab/planx-core/types";
import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { useCurrentRoute, useNavigation } from "react-navi";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";
import EditorIcon from "ui/icons/Editor";

interface Route {
title: string;
Icon: React.ElementType;
route: string;
accessibleBy: Role[];
}

const MENU_WIDTH_COMPACT = "52px";
const MENU_WIDTH_FULL = "178px";

const Root = styled(Box, {
shouldForwardProp: (prop) => prop !== "compact",
})<{ compact?: boolean }>(({ theme, compact }) => ({
width: compact ? MENU_WIDTH_COMPACT : MENU_WIDTH_FULL,
flexShrink: 0,
background: theme.palette.background.paper,
borderRight: `1px solid ${theme.palette.border.light}`,
}));

const MenuWrap = styled("ul")(({ theme }) => ({
listStyle: "none",
margin: 0,
padding: theme.spacing(1, 0.4, 0, 0.4),
position: "sticky",
top: 0,
}));

const MenuItem = styled("li")(({ theme }) => ({
margin: theme.spacing(0.75, 0),
padding: 0,
}));

const MenuTitle = styled(Typography)(({ theme }) => ({
fontWeight: FONT_WEIGHT_SEMI_BOLD,
paddingLeft: theme.spacing(0.5),
textAlign: "left",
})) as typeof Typography;

const TooltipWrap = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} arrow placement="right" classes={{ popper: className }} />
))(() => ({
[`& .${tooltipClasses.arrow}`]: {
color: "#2c2c2c",
},
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "#2c2c2c",
left: "-5px",
fontSize: "0.8em",
borderRadius: 0,
fontWeight: FONT_WEIGHT_SEMI_BOLD,
},
}));

const MenuButton = styled(IconButton, {
shouldForwardProp: (prop) => prop !== "isActive",
})<{ isActive: boolean }>(({ theme, isActive }) => ({
color: theme.palette.text.primary,
width: "100%",
border: "1px solid transparent",
justifyContent: "flex-start",
borderRadius: "3px",
"&:hover": {
background: "white",
borderColor: theme.palette.border.light,
},
...(isActive && {
background: theme.palette.common.white,
color: theme.palette.text.primary,
border: `1px solid ${theme.palette.border.main}`,
}),
"& > svg": {
opacity: 0.8,
},
}));

function EditorNavMenu() {
const { navigate } = useNavigation();
const { url } = useCurrentRoute();
const [teamSlug, flowSlug, user, canUserEditTeam] = useStore((state) => [
state.teamSlug,
state.flowSlug,
state.user,
state.canUserEditTeam,
]);

const isActive = (route: string) => url.href.endsWith(route);

const handleClick = (route: string) => {
if (isActive(route)) return;
navigate(route);
};

const globalLayoutRoutes: Route[] = [
{
title: "Select a team",
Icon: FormatListBulletedIcon,
route: "/",
accessibleBy: ["platformAdmin", "teamEditor", "teamViewer"],
},
{
title: "Admin panel",
Icon: AdminPanelSettingsIcon,
route: "admin-panel",
accessibleBy: ["platformAdmin"],
},
{
title: "Global settings",
Icon: TuneIcon,
route: "global-settings",
accessibleBy: ["platformAdmin"],
},
];

const teamLayoutRoutes: Route[] = [
{
title: "Services",
Icon: FormatListBulletedIcon,
route: `/${teamSlug}`,
accessibleBy: ["platformAdmin", "teamEditor", "teamViewer"],
},
{
title: "Team members",
Icon: GroupIcon,
route: `/${teamSlug}/members`,
accessibleBy: ["platformAdmin", "teamEditor"],
},
{
title: "Design",
Icon: PaletteIcon,
route: `/${teamSlug}/design`,
accessibleBy: ["platformAdmin", "teamEditor"],
},
{
title: "Settings",
Icon: TuneIcon,
route: `/${teamSlug}/general-settings`,
accessibleBy: ["platformAdmin", "teamEditor"],
},
];

const flowLayoutRoutes: Route[] = [
{
title: "Editor",
Icon: EditorIcon,
route: `/${teamSlug}/${flowSlug}`,
accessibleBy: ["platformAdmin", "teamEditor", "teamViewer"],
},
{
title: "Service settings",
Icon: TuneIcon,
route: `/${teamSlug}/${flowSlug}/service`,
accessibleBy: ["platformAdmin", "teamEditor"],
},
{
title: "Submissions log",
Icon: FactCheckIcon,
route: `/${teamSlug}/${flowSlug}/submissions-log`,
accessibleBy: ["platformAdmin", "teamEditor"],
},
{
title: "Feedback",
Icon: RateReviewIcon,
route: `/${teamSlug}/${flowSlug}/feedback`,
accessibleBy: ["platformAdmin", "teamEditor"],
},
];

const getRoutesForUrl = (
url: string,
): { routes: Route[]; compact: boolean } => {
if (flowSlug && url.includes(flowSlug))
return { routes: flowLayoutRoutes, compact: true };
if (teamSlug && url.includes(teamSlug))
return { routes: teamLayoutRoutes, compact: false };
return { routes: globalLayoutRoutes, compact: false };
};

const { routes, compact } = getRoutesForUrl(url.href);

const visibleRoutes = routes.filter(({ accessibleBy }) => {
if (user?.isPlatformAdmin) return accessibleBy.includes("platformAdmin");
if (canUserEditTeam(teamSlug)) return accessibleBy.includes("teamEditor");
return accessibleBy.includes("teamViewer");
});

// Hide menu if the user does not have a selection of items
if (visibleRoutes.length < 2) return null;

return (
<Root compact={compact}>
<MenuWrap>
{visibleRoutes.map(({ title, Icon, route }) => (
<MenuItem onClick={() => handleClick(route)} key={title}>
{compact ? (
<TooltipWrap title={title}>
<MenuButton isActive={isActive(route)} disableRipple>
<Icon />
</MenuButton>
</TooltipWrap>
) : (
<MenuButton isActive={isActive(route)} disableRipple>
<Icon />
<MenuTitle variant="body2">{title}</MenuTitle>
</MenuButton>
)}
</MenuItem>
))}
</MenuWrap>
</Root>
);
}

export default EditorNavMenu;
80 changes: 3 additions & 77 deletions editor.planx.uk/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import Permission from "ui/editor/Permission";
import Reset from "ui/icons/Reset";

import { useStore } from "../pages/FlowEditor/lib/store";
import { rootFlowPath, rootTeamPath } from "../routes/utils";
import { rootFlowPath } from "../routes/utils";
import AnalyticsDisabledBanner from "./AnalyticsDisabledBanner";
import { ConfirmationDialog } from "./ConfirmationDialog";
import TestEnvironmentBanner from "./TestEnvironmentBanner";
Expand All @@ -63,7 +63,7 @@ const BreadcrumbsRoot = styled(Box)(() => ({
const BreadcrumbsLink = styled(Link)(({ theme }) => ({
color: theme.palette.common.white,
textDecoration: "none",
borderBottom: "1px solid currentColor",
borderBottom: "1px solid rgba(255, 255, 255, 0.75)",
})) as typeof Link;

const StyledToolbar = styled(MuiToolbar)(() => ({
Expand Down Expand Up @@ -436,13 +436,6 @@ const EditorToolbar: React.FC<{
setOpen(!open);
};

const isFlowSettingsVisible = route.data.flow && canUserEditTeam(team.slug);

const isTeamSettingsVisible =
route.data.team && !route.data.flow && canUserEditTeam(team.slug);

const isGlobalSettingsVisible = !route.data.team && user?.isPlatformAdmin;

return (
<>
<StyledToolbar disableGutters>
Expand Down Expand Up @@ -488,7 +481,7 @@ const EditorToolbar: React.FC<{
{user.lastName[0]}
</Avatar>
<Typography variant="body2" fontSize="small">
Menu
Account
</Typography>
<KeyboardArrowDown />
</IconButton>
Expand Down Expand Up @@ -520,73 +513,6 @@ const EditorToolbar: React.FC<{
</ListItemIcon>
<ListItemText>{user.email}</ListItemText>
</MenuItem>
{(user.isPlatformAdmin || user.teams.length > 0) && (
<MenuItem disabled>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText>
{user.isPlatformAdmin
? `All teams`
: user.teams.map((team) => team.team.name).join(", ")}
</ListItemText>
</MenuItem>
)}
<Permission.IsNotPlatformAdmin>
<MenuItem disabled divider>
<ListItemIcon>
<Visibility />
</ListItemIcon>
<ListItemText>All teams</ListItemText>
</MenuItem>
</Permission.IsNotPlatformAdmin>

{/* Only show team settings link if inside a team route */}
{isTeamSettingsVisible && (
<>
<MenuItem
onClick={() => navigate(`${rootTeamPath()}/settings`)}
>
Team Settings
</MenuItem>
<MenuItem onClick={() => navigate(`${rootTeamPath()}/members`)}>
Team Members
</MenuItem>
</>
)}

{/* Only show flow settings link if inside a flow route */}
{isFlowSettingsVisible && (
<>
<MenuItem
onClick={() =>
navigate([rootFlowPath(true), "settings"].join("/"))
}
>
Flow Settings
</MenuItem>
<MenuItem
onClick={() =>
navigate([rootFlowPath(true), "feedback"].join("/"))
}
>
Feedback
</MenuItem>
</>
)}

{/* Only show global settings & admin panel links from top-level view */}
{isGlobalSettingsVisible && (
<>
<MenuItem onClick={() => navigate("/global-settings")}>
Global Settings
</MenuItem>
<MenuItem onClick={() => navigate("/admin-panel")}>
Admin Panel
</MenuItem>
</>
)}

<MenuItem onClick={() => navigate("/logout")}>Log out</MenuItem>
</StyledPaper>
</StyledPopover>
Expand Down
2 changes: 1 addition & 1 deletion editor.planx.uk/src/lib/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// add/edit/remove feature flags in array below
const AVAILABLE_FEATURE_FLAGS = ["EDITOR_NAVIGATION"] as const;
const AVAILABLE_FEATURE_FLAGS = [] as const;

type FeatureFlag = (typeof AVAILABLE_FEATURE_FLAGS)[number];

Expand Down
Loading

0 comments on commit b484df1

Please sign in to comment.