feat: add a divider after Account menu item (#1927)

* add a divider after Account menu item

* test: improve Storybook tests

* add closed and open userdropdown tests

* add default isOpen

* extract UserDropdownContent into a single component

* remove the isOpen prop

* address nit comments

* update test name
This commit is contained in:
Abhineet Jain
2022-06-02 11:09:19 -04:00
committed by GitHub
parent e6ee7dd652
commit 47c7eda670
7 changed files with 223 additions and 203 deletions
@@ -17,8 +17,8 @@ const Template: Story<UserDropdownProps> = (args: UserDropdownProps) => (
</Box>
)
export const ExampleNoRoles = Template.bind({})
ExampleNoRoles.args = {
export const Example = Template.bind({})
Example.args = {
user: MockUser,
onSignOut: () => {
return Promise.resolve()
@@ -1,7 +1,8 @@
import { screen } from "@testing-library/react"
import { MockAdminRole, MockUser } from "../../testHelpers/entities"
import { MockUser } from "../../testHelpers/entities"
import { render } from "../../testHelpers/renderHelpers"
import { Language, UserDropdown, UserDropdownProps } from "./UsersDropdown"
import { Language } from "../UserDropdownContent/UserDropdownContent"
import { UserDropdown, UserDropdownProps } from "./UsersDropdown"
const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
render(<UserDropdown user={props.user ?? MockUser} onSignOut={props.onSignOut ?? jest.fn()} />)
@@ -10,18 +11,6 @@ const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
}
describe("UserDropdown", () => {
const env = process.env
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
beforeEach(() => {
process.env = { ...env }
})
// REMARK: restoring process.env
afterEach(() => {
process.env = env
})
describe("when the trigger is clicked", () => {
it("opens the menu", async () => {
await renderAndClick()
@@ -30,44 +19,4 @@ describe("UserDropdown", () => {
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
})
})
describe("when the menu is open", () => {
it("displays the user's roles", async () => {
await renderAndClick()
expect(screen.getByText(MockAdminRole.display_name)).toBeDefined()
})
it("has the correct link for the documentation item", async () => {
process.env.CODER_VERSION = "v0.5.4"
await renderAndClick()
const link = screen.getByText(Language.docsLabel).closest("a")
if (!link) {
throw new Error("Anchor tag not found for the documentation menu item")
}
expect(link.getAttribute("href")).toBe(`https://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`)
})
it("has the correct link for the account item", async () => {
await renderAndClick()
const link = screen.getByText(Language.accountLabel).closest("a")
if (!link) {
throw new Error("Anchor tag not found for the account menu item")
}
expect(link.getAttribute("href")).toBe("/settings/account")
})
describe("and sign out is clicked", () => {
it("calls the onSignOut function", async () => {
const onSignOut = jest.fn()
await renderAndClick({ onSignOut })
screen.getByText(Language.signOutLabel).click()
expect(onSignOut).toBeCalledTimes(1)
})
})
})
})
@@ -1,26 +1,14 @@
import Badge from "@material-ui/core/Badge"
import Divider from "@material-ui/core/Divider"
import ListItemIcon from "@material-ui/core/ListItemIcon"
import ListItemText from "@material-ui/core/ListItemText"
import MenuItem from "@material-ui/core/MenuItem"
import { fade, makeStyles } from "@material-ui/core/styles"
import AccountIcon from "@material-ui/icons/AccountCircleOutlined"
import React, { useState } from "react"
import { Link } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { navHeight } from "../../theme/constants"
import { BorderedMenu } from "../BorderedMenu/BorderedMenu"
import { CloseDropdown, OpenDropdown } from "../DropdownArrows/DropdownArrows"
import { DocsIcon } from "../Icons/DocsIcon"
import { LogoutIcon } from "../Icons/LogoutIcon"
import { UserAvatar } from "../UserAvatar/UserAvatar"
import { UserProfileCard } from "../UserProfileCard/UserProfileCard"
import { UserDropdownContent } from "../UserDropdownContent/UserDropdownContent"
export const Language = {
accountLabel: "Account",
docsLabel: "Documentation",
signOutLabel: "Sign Out",
}
export interface UserDropdownProps {
user: TypesGen.User
onSignOut: () => void
@@ -64,41 +52,7 @@ export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: U
variant="user-dropdown"
onClose={onPopoverClose}
>
<div className={styles.userInfo}>
<UserProfileCard user={user} />
<Divider />
<Link to="/settings/account" className={styles.link}>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
<ListItemIcon className={styles.icon}>
<AccountIcon />
</ListItemIcon>
<ListItemText primary={Language.accountLabel} />
</MenuItem>
</Link>
<a
href={`https://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`}
target="_blank"
rel="noreferrer"
className={styles.link}
>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
<ListItemIcon className={styles.icon}>
<DocsIcon />
</ListItemIcon>
<ListItemText primary={Language.docsLabel} />
</MenuItem>
</a>
<MenuItem className={styles.menuItem} onClick={onSignOut}>
<ListItemIcon className={styles.icon}>
<LogoutIcon />
</ListItemIcon>
<ListItemText primary={Language.signOutLabel} />
</MenuItem>
</div>
<UserDropdownContent user={user} onPopoverClose={onPopoverClose} onSignOut={onSignOut} />
</BorderedMenu>
</>
)
@@ -117,10 +71,6 @@ export const useStyles = makeStyles((theme) => ({
maxWidth: 300,
},
userInfo: {
marginBottom: theme.spacing(1),
},
menuItem: {
height: navHeight,
padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`,
@@ -130,13 +80,4 @@ export const useStyles = makeStyles((theme) => ({
transition: "background-color 0.3s ease",
},
},
link: {
textDecoration: "none",
color: "inherit",
},
icon: {
color: theme.palette.text.secondary,
},
}))
@@ -1,17 +1,14 @@
import { Story } from "@storybook/react"
import React from "react"
import { MockUser } from "../../testHelpers/entities"
import { UserProfileCard, UserProfileCardProps } from "./UserProfileCard"
import { UserDropdownContent, UserDropdownContentProps } from "./UserDropdownContent"
export default {
title: "components/UserDropdown",
component: UserProfileCard,
argTypes: {
onSignOut: { action: "Sign Out" },
},
title: "components/UserDropdownContent",
component: UserDropdownContent,
}
const Template: Story<UserProfileCardProps> = (args: UserProfileCardProps) => <UserProfileCard {...args} />
const Template: Story<UserDropdownContentProps> = (args) => <UserDropdownContent {...args} />
export const ExampleNoRoles = Template.bind({})
ExampleNoRoles.args = {
@@ -0,0 +1,61 @@
import { screen } from "@testing-library/react"
import { MockAdminRole, MockUser } from "../../testHelpers/entities"
import { render } from "../../testHelpers/renderHelpers"
import { Language, UserDropdownContent } from "./UserDropdownContent"
describe("UserDropdownContent", () => {
const env = process.env
// REMARK: copying process.env so we don't mutate that object or encounter conflicts between tests
beforeEach(() => {
process.env = { ...env }
})
// REMARK: restoring process.env
afterEach(() => {
process.env = env
})
it("displays the menu items", () => {
render(<UserDropdownContent user={MockUser} onSignOut={jest.fn()} onPopoverClose={jest.fn()} />)
expect(screen.getByText(Language.accountLabel)).toBeDefined()
expect(screen.getByText(Language.docsLabel)).toBeDefined()
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
})
it("displays the user's roles", () => {
render(<UserDropdownContent user={MockUser} onSignOut={jest.fn()} onPopoverClose={jest.fn()} />)
expect(screen.getByText(MockAdminRole.display_name)).toBeDefined()
})
it("has the correct link for the documentation item", () => {
process.env.CODER_VERSION = "v0.5.4"
render(<UserDropdownContent user={MockUser} onSignOut={jest.fn()} onPopoverClose={jest.fn()} />)
const link = screen.getByText(Language.docsLabel).closest("a")
if (!link) {
throw new Error("Anchor tag not found for the documentation menu item")
}
expect(link.getAttribute("href")).toBe(`https://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`)
})
it("has the correct link for the account item", () => {
render(<UserDropdownContent user={MockUser} onSignOut={jest.fn()} onPopoverClose={jest.fn()} />)
const link = screen.getByText(Language.accountLabel).closest("a")
if (!link) {
throw new Error("Anchor tag not found for the account menu item")
}
expect(link.getAttribute("href")).toBe("/settings/account")
})
it("calls the onSignOut function", () => {
const onSignOut = jest.fn()
render(<UserDropdownContent user={MockUser} onSignOut={onSignOut} onPopoverClose={jest.fn()} />)
screen.getByText(Language.signOutLabel).click()
expect(onSignOut).toBeCalledTimes(1)
})
})
@@ -0,0 +1,151 @@
import Chip from "@material-ui/core/Chip"
import Divider from "@material-ui/core/Divider"
import ListItemIcon from "@material-ui/core/ListItemIcon"
import ListItemText from "@material-ui/core/ListItemText"
import MenuItem from "@material-ui/core/MenuItem"
import { fade, makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import AccountIcon from "@material-ui/icons/AccountCircleOutlined"
import { FC } from "react"
import { Link } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { Role } from "../../api/typesGenerated"
import { navHeight } from "../../theme/constants"
import { DocsIcon } from "../Icons/DocsIcon"
import { LogoutIcon } from "../Icons/LogoutIcon"
import { UserAvatar } from "../UserAvatar/UserAvatar"
export const Language = {
accountLabel: "Account",
docsLabel: "Documentation",
signOutLabel: "Sign Out",
}
export interface UserDropdownContentProps {
user: TypesGen.User
onPopoverClose: () => void
onSignOut: () => void
}
export const UserDropdownContent: FC<UserDropdownContentProps> = ({ user, onPopoverClose, onSignOut }) => {
const styles = useStyles()
return (
<div className={styles.userInfo}>
<div className={styles.root}>
<div className={styles.avatarContainer}>
<UserAvatar className={styles.avatar} username={user.username} />
</div>
<Typography className={styles.userName}>{user.username}</Typography>
<Typography className={styles.userEmail}>{user.email}</Typography>
<ul className={styles.chipContainer}>
{user.roles.map((role: Role) => (
<li key={role.name} className={styles.chipStyles}>
<Chip classes={{ root: styles.chipRoot }} label={role.display_name} />
</li>
))}
</ul>
</div>
<Divider />
<Link to="/settings/account" className={styles.link}>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
<ListItemIcon className={styles.icon}>
<AccountIcon />
</ListItemIcon>
<ListItemText primary={Language.accountLabel} />
</MenuItem>
</Link>
<Divider />
<a
href={`https://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`}
target="_blank"
rel="noreferrer"
className={styles.link}
>
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
<ListItemIcon className={styles.icon}>
<DocsIcon />
</ListItemIcon>
<ListItemText primary={Language.docsLabel} />
</MenuItem>
</a>
<MenuItem className={styles.menuItem} onClick={onSignOut}>
<ListItemIcon className={styles.icon}>
<LogoutIcon />
</ListItemIcon>
<ListItemText primary={Language.signOutLabel} />
</MenuItem>
</div>
)
}
const useStyles = makeStyles((theme) => ({
root: {
paddingTop: theme.spacing(3),
textAlign: "center",
},
avatarContainer: {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
avatar: {
width: 48,
height: 48,
borderRadius: "50%",
marginBottom: theme.spacing(1),
transition: `transform .2s`,
"&:hover": {
transform: `scale(1.1)`,
},
},
userName: {
fontSize: 16,
marginBottom: theme.spacing(0.5),
},
userEmail: {
fontSize: 14,
letterSpacing: 0.2,
color: theme.palette.text.secondary,
},
chipContainer: {
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
listStyle: "none",
margin: "0",
padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`,
},
chipStyles: {
margin: theme.spacing(0.5),
},
chipRoot: {
backgroundColor: "#7057FF",
},
link: {
textDecoration: "none",
color: "inherit",
},
menuItem: {
height: navHeight,
padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`,
"&:hover": {
backgroundColor: fade(theme.palette.primary.light, 0.1),
transition: "background-color 0.3s ease",
},
},
userInfo: {
marginBottom: theme.spacing(1),
},
icon: {
color: theme.palette.text.secondary,
},
}))
@@ -1,79 +0,0 @@
import Chip from "@material-ui/core/Chip"
import { makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import { Role } from "../../api/typesGenerated"
import { UserAvatar } from "../UserAvatar/UserAvatar"
export interface UserProfileCardProps {
user: TypesGen.User
}
export const UserProfileCard: FC<UserProfileCardProps> = ({ user }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<div className={styles.avatarContainer}>
<UserAvatar className={styles.avatar} username={user.username} />
</div>
<Typography className={styles.userName}>{user.username}</Typography>
<Typography className={styles.userEmail}>{user.email}</Typography>
<ul className={styles.chipContainer}>
{user.roles.map((role: Role) => (
<li key={role.name} className={styles.chipStyles}>
<Chip classes={{ root: styles.chipRoot }} label={role.display_name} />
</li>
))}
</ul>
</div>
)
}
const useStyles = makeStyles((theme) => ({
root: {
paddingTop: theme.spacing(3),
textAlign: "center",
},
avatarContainer: {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
avatar: {
width: 48,
height: 48,
borderRadius: "50%",
marginBottom: theme.spacing(1),
transition: `transform .2s`,
"&:hover": {
transform: `scale(1.1)`,
},
},
userName: {
fontSize: 16,
marginBottom: theme.spacing(0.5),
},
userEmail: {
fontSize: 14,
letterSpacing: 0.2,
color: theme.palette.text.secondary,
},
chipContainer: {
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
listStyle: "none",
margin: "0",
padding: `${theme.spacing(1.5)}px ${theme.spacing(2.75)}px`,
},
chipStyles: {
margin: theme.spacing(0.5),
},
chipRoot: {
backgroundColor: "#7057FF",
},
}))