Skip to content

refactor(site): make minor design tweaks and fix issues on more options menus #10493

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

Merged
merged 13 commits into from
Nov 3, 2023
108 changes: 108 additions & 0 deletions site/src/components/MoreMenu/MoreMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useRef, useState, createContext, useContext, ReactNode } from "react";
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined";
import Menu, { MenuProps } from "@mui/material/Menu";
import MenuItem, { MenuItemProps } from "@mui/material/MenuItem";
import IconButton, { IconButtonProps } from "@mui/material/IconButton";

type MoreMenuContextValue = {
triggerRef: React.RefObject<HTMLButtonElement>;
close: () => void;
open: () => void;
isOpen: boolean;
};

const MoreMenuContext = createContext<MoreMenuContextValue | undefined>(
undefined,
);

export const MoreMenu = (props: { children: ReactNode }) => {
const triggerRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false);

const close = () => {
setIsOpen(false);
};

const open = () => {
setIsOpen(true);
};

return (
<MoreMenuContext.Provider value={{ close, open, triggerRef, isOpen }}>
{props.children}
</MoreMenuContext.Provider>
);
};

const useMoreMenuContext = () => {
const ctx = useContext(MoreMenuContext);

if (!ctx) {
throw new Error("useMoreMenuContext must be used inside of MoreMenu");
}

return ctx;
};

export const MoreMenuTrigger = (props: IconButtonProps) => {
const menu = useMoreMenuContext();

return (
<IconButton
aria-controls="more-options"
aria-label="More options"
aria-haspopup="true"
onClick={menu.open}
ref={menu.triggerRef}
{...props}
>
<MoreVertOutlined />
</IconButton>
);
};

export const MoreMenuContent = (props: Omit<MenuProps, "open" | "onClose">) => {
const menu = useMoreMenuContext();

return (
<Menu
id="more-options"
anchorEl={menu.triggerRef.current}
open={menu.isOpen}
onClose={menu.close}
disablePortal
{...props}
/>
);
};

export const MoreMenuItem = (
props: MenuItemProps & { closeOnClick?: boolean; danger?: boolean },
) => {
const { closeOnClick = true, danger = false, ...menuItemProps } = props;
const ctx = useContext(MoreMenuContext);

if (!ctx) {
throw new Error("MoreMenuItem must be used inside of MoreMenu");
}

return (
<MenuItem
{...menuItemProps}
css={(theme) => ({
fontSize: 14,
color: danger ? theme.palette.error.light : undefined,
"& .MuiSvgIcon-root": {
width: theme.spacing(2),
height: theme.spacing(2),
},
})}
onClick={(e) => {
menuItemProps.onClick && menuItemProps.onClick(e);
if (closeOnClick) {
ctx.close();
}
}}
/>
);
};
24 changes: 0 additions & 24 deletions site/src/components/TableRowMenu/TableRowMenu.stories.tsx

This file was deleted.

63 changes: 0 additions & 63 deletions site/src/components/TableRowMenu/TableRowMenu.tsx

This file was deleted.

31 changes: 19 additions & 12 deletions site/src/pages/GroupsPage/GroupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import { TableRowMenu } from "components/TableRowMenu/TableRowMenu";
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
import { type FC, useState } from "react";
import { Helmet } from "react-helmet-async";
Expand All @@ -46,6 +45,12 @@ import Box from "@mui/material/Box";
import { LastSeen } from "components/LastSeen/LastSeen";
import { type Interpolation, type Theme } from "@emotion/react";
import LoadingButton from "@mui/lab/LoadingButton";
import {
MoreMenu,
MoreMenuContent,
MoreMenuItem,
MoreMenuTrigger,
} from "components/MoreMenu/MoreMenu";

export const GroupPage: FC = () => {
const { groupId } = useParams() as { groupId: string };
Expand Down Expand Up @@ -281,12 +286,12 @@ const GroupMemberRow = (props: {
</TableCell>
<TableCell width="1%">
{canUpdate && (
<TableRowMenu
data={member}
menuItems={[
{
label: "Remove",
onClick: async () => {
<MoreMenu>
<MoreMenuTrigger />
<MoreMenuContent>
<MoreMenuItem
danger
onClick={async () => {
try {
await removeMemberMutation.mutateAsync({
groupId: group.id,
Expand All @@ -298,11 +303,13 @@ const GroupMemberRow = (props: {
getErrorMessage(error, "Failed to remove member."),
);
}
},
disabled: group.id === group.organization_id,
},
]}
/>
}}
disabled={group.id === group.organization_id}
>
Remove
</MoreMenuItem>
</MoreMenuContent>
</MoreMenu>
)}
</TableCell>
</TableRow>
Expand Down
86 changes: 31 additions & 55 deletions site/src/pages/TemplatePage/TemplatePageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type FC, useRef, useState } from "react";
import { type FC } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { useDeletionDialogState } from "./useDeletionDialogState";

Expand All @@ -20,17 +20,19 @@ import {
PageHeaderTitle,
PageHeaderSubtitle,
} from "components/PageHeader/PageHeader";

import Button from "@mui/material/Button";
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import IconButton from "@mui/material/IconButton";
import AddIcon from "@mui/icons-material/AddOutlined";
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import EditIcon from "@mui/icons-material/EditOutlined";
import CopyIcon from "@mui/icons-material/FileCopyOutlined";
import {
MoreMenu,
MoreMenuContent,
MoreMenuItem,
MoreMenuTrigger,
} from "components/MoreMenu/MoreMenu";
import Divider from "@mui/material/Divider";

type TemplateMenuProps = {
templateName: string;
Expand All @@ -46,80 +48,54 @@ const TemplateMenu: FC<TemplateMenuProps> = ({
onDelete,
}) => {
const dialogState = useDeletionDialogState(templateId, onDelete);
const menuTriggerRef = useRef<HTMLButtonElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const navigate = useNavigate();

const queryText = `template:${templateName}`;
const workspaceCountQuery = useQuery({
...workspaces({ q: queryText }),
select: (res) => res.count,
});

// Returns a function that will execute the action and close the menu
const onMenuItemClick = (actionFn: () => void) => () => {
setIsMenuOpen(false);
actionFn();
};

const safeToDeleteTemplate = workspaceCountQuery.data === 0;

return (
<>
<div>
<IconButton
aria-controls="template-options"
aria-haspopup="true"
onClick={() => setIsMenuOpen(true)}
ref={menuTriggerRef}
arial-label="More options"
>
<MoreVertOutlined />
</IconButton>

<Menu
id="template-options"
anchorEl={menuTriggerRef.current}
open={isMenuOpen}
onClose={() => setIsMenuOpen(false)}
>
<MenuItem
onClick={onMenuItemClick(() =>
navigate(`/templates/${templateName}/settings`),
)}
<MoreMenu>
<MoreMenuTrigger />
<MoreMenuContent>
<MoreMenuItem
onClick={() => {
navigate(`/templates/${templateName}/settings`);
}}
>
<SettingsIcon />
Settings
</MenuItem>
</MoreMenuItem>

<MenuItem
onClick={onMenuItemClick(() =>
<MoreMenuItem
onClick={() => {
navigate(
`/templates/${templateName}/versions/${templateVersion}/edit`,
),
)}
);
}}
>
<EditIcon />
Edit files
</MenuItem>
</MoreMenuItem>

<MenuItem
onClick={onMenuItemClick(() =>
navigate(`/templates/new?fromTemplate=${templateName}`),
)}
<MoreMenuItem
onClick={() => {
navigate(`/templates/new?fromTemplate=${templateName}`);
}}
>
<CopyIcon />
Duplicate&hellip;
</MenuItem>

<MenuItem
onClick={onMenuItemClick(dialogState.openDeleteConfirmation)}
>
</MoreMenuItem>
<Divider />
<MoreMenuItem onClick={dialogState.openDeleteConfirmation} danger>
<DeleteIcon />
Delete&hellip;
</MenuItem>
</Menu>
</div>
</MoreMenuItem>
</MoreMenuContent>
</MoreMenu>

{safeToDeleteTemplate ? (
<DeleteDialog
Expand Down
Loading