Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit90388a3

Browse files
feat: Add user menu (#887)
1 parent2ca7253 commit90388a3

File tree

6 files changed

+158
-3
lines changed

6 files changed

+158
-3
lines changed

‎site/src/AppRouter.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NotFoundPage } from "./pages/404"
66
import{CliAuthenticationPage}from"./pages/cli-auth"
77
import{HealthzPage}from"./pages/healthz"
88
import{SignInPage}from"./pages/login"
9+
import{PreferencesPage}from"./pages/preferences"
910
import{TemplatesPage}from"./pages/templates"
1011
import{TemplatePage}from"./pages/templates/[organization]/[template]"
1112
import{CreateWorkspacePage}from"./pages/templates/[organization]/[template]/create"
@@ -67,6 +68,17 @@ export const AppRouter: React.FC = () => (
6768
/>
6869
</Route>
6970

71+
<Routepath="preferences">
72+
<Route
73+
index
74+
element={
75+
<AuthAndNav>
76+
<PreferencesPage/>
77+
</AuthAndNav>
78+
}
79+
/>
80+
</Route>
81+
7082
{/* Using path="*"" means "match anything", so this route
7183
acts like a catch-all for URLs that we don't have explicit
7284
routes for. */}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
importSvgIcon,{SvgIconProps}from"@material-ui/core/SvgIcon"
2+
importReactfrom"react"
3+
4+
exportconstDocsIcon=(props:SvgIconProps):JSX.Element=>(
5+
<SvgIcon{...props}viewBox="0 0 24 24">
6+
<path
7+
fillRule="evenodd"
8+
clipRule="evenodd"
9+
d="M6.53846 3.75C4.67698 3.75 2.86058 4.50721 2.86058 4.50721L2.5 4.66947V16.4423H9.00841C9.20898 16.7871 9.57407 17.0192 10 17.0192C10.4259 17.0192 10.791 16.7871 10.9916 16.4423H17.5V4.66947L17.1394 4.50721C17.1394 4.50721 15.323 3.75 13.4615 3.75C11.7781 3.75 10.2997 4.31566 10 4.4351C9.70027 4.31566 8.22191 3.75 6.53846 3.75ZM6.53846 4.90385C7.654 4.90385 8.84615 5.26442 9.42308 5.46274V14.7656C8.7808 14.5538 7.72611 14.2608 6.53846 14.2608C5.32602 14.2608 4.33894 14.5403 3.65385 14.7656V5.46274C4.09781 5.30273 5.26968 4.90385 6.53846 4.90385ZM13.4615 4.90385C14.7303 4.90385 15.9022 5.30273 16.3462 5.46274V14.7656C15.6611 14.5403 14.674 14.2608 13.4615 14.2608C12.2739 14.2608 11.2192 14.5538 10.5769 14.7656V5.46274C11.1538 5.26442 12.346 4.90385 13.4615 4.90385Z"
10+
fill="currentColor"
11+
/>
12+
</SvgIcon>
13+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
importBoxfrom"@material-ui/core/Box"
2+
import{Story}from"@storybook/react"
3+
importReactfrom"react"
4+
import{UserDropdown,UserDropdownProps}from"./UserDropdown"
5+
6+
exportdefault{
7+
title:"Page/UserDropdown",
8+
component:UserDropdown,
9+
argTypes:{
10+
onSignOut:{action:"Sign Out"},
11+
},
12+
}
13+
14+
constTemplate:Story<UserDropdownProps>=(args:UserDropdownProps)=>(
15+
<Boxstyle={{backgroundColor:"#000",width:88}}>
16+
<UserDropdown{...args}/>
17+
</Box>
18+
)
19+
20+
exportconstExample=Template.bind({})
21+
Example.args={
22+
user:{id:"1",username:"CathyCoder",email:"cathy@coder.com",created_at:"dawn"},
23+
onSignOut:()=>{
24+
returnPromise.resolve()
25+
},
26+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import{screen}from"@testing-library/react"
2+
importReactfrom"react"
3+
import{render}from"../../test_helpers"
4+
import{MockUser}from"../../test_helpers/entities"
5+
import{Language,UserDropdown,UserDropdownProps}from"./UserDropdown"
6+
7+
constrenderAndClick=async(props:Partial<UserDropdownProps>={})=>{
8+
render(<UserDropdownuser={props.user??MockUser}onSignOut={props.onSignOut??jest.fn()}/>)
9+
consttrigger=awaitscreen.findByTestId("user-dropdown-trigger")
10+
trigger.click()
11+
}
12+
13+
describe("UserDropdown",()=>{
14+
describe("when the trigger is clicked",()=>{
15+
it("opens the menu",async()=>{
16+
awaitrenderAndClick()
17+
expect(screen.getByText(Language.accountLabel)).toBeDefined()
18+
expect(screen.getByText(Language.docsLabel)).toBeDefined()
19+
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
20+
})
21+
})
22+
23+
describe("when the menu is open",()=>{
24+
describe("and sign out is clicked",()=>{
25+
it("calls the onSignOut function",async()=>{
26+
constonSignOut=jest.fn()
27+
awaitrenderAndClick({ onSignOut})
28+
screen.getByText(Language.signOutLabel).click()
29+
expect(onSignOut).toBeCalledTimes(1)
30+
})
31+
})
32+
})
33+
34+
it("has the correct link for the documentation item",async()=>{
35+
awaitrenderAndClick()
36+
37+
constlink=screen.getByText(Language.docsLabel).closest("a")
38+
if(!link){
39+
thrownewError("Anchor tag not found for the documentation menu item")
40+
}
41+
42+
expect(link.getAttribute("href")).toBe("https://coder.com/docs")
43+
})
44+
45+
it("has the correct link for the account item",async()=>{
46+
awaitrenderAndClick()
47+
48+
constlink=screen.getByText(Language.accountLabel).closest("a")
49+
if(!link){
50+
thrownewError("Anchor tag not found for the account menu item")
51+
}
52+
53+
expect(link.getAttribute("href")).toBe("/preferences")
54+
})
55+
})

‎site/src/components/Navbar/UserDropdown.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@ import ListItemIcon from "@material-ui/core/ListItemIcon"
44
importListItemTextfrom"@material-ui/core/ListItemText"
55
importMenuItemfrom"@material-ui/core/MenuItem"
66
import{fade,makeStyles}from"@material-ui/core/styles"
7+
importAccountIconfrom"@material-ui/icons/AccountCircleOutlined"
78
importKeyboardArrowDownfrom"@material-ui/icons/KeyboardArrowDown"
89
importKeyboardArrowUpfrom"@material-ui/icons/KeyboardArrowUp"
910
importReact,{useState}from"react"
11+
import{Link}from"react-router-dom"
1012
import{UserResponse}from"../../api/types"
1113
import{LogoutIcon}from"../Icons"
14+
import{DocsIcon}from"../Icons/DocsIcon"
1215
import{UserAvatar}from"../User"
1316
import{UserProfileCard}from"../User/UserProfileCard"
1417
import{BorderedMenu}from"./BorderedMenu"
1518

19+
exportconstLanguage={
20+
accountLabel:"Account",
21+
docsLabel:"Documentation",
22+
signOutLabel:"Sign Out",
23+
}
1624
exportinterfaceUserDropdownProps{
1725
user:UserResponse
1826
onSignOut:()=>void
@@ -32,7 +40,7 @@ export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: U
3240
return(
3341
<>
3442
<div>
35-
<MenuItemonClick={handleDropdownClick}>
43+
<MenuItemonClick={handleDropdownClick}data-testid="user-dropdown-trigger">
3644
<divclassName={styles.inner}>
3745
<Badgeoverlap="circle">
3846
<UserAvatarusername={user.username}/>
@@ -65,13 +73,31 @@ export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: U
6573
<divclassName={styles.userInfo}>
6674
<UserProfileCarduser={user}/>
6775

68-
<DividerclassName={styles.divider}/>
76+
<Divider/>
77+
78+
<Linkto="/preferences"className={styles.link}>
79+
<MenuItemclassName={styles.menuItem}onClick={handleDropdownClick}>
80+
<ListItemIconclassName={styles.icon}>
81+
<AccountIcon/>
82+
</ListItemIcon>
83+
<ListItemTextprimary={Language.accountLabel}/>
84+
</MenuItem>
85+
</Link>
86+
87+
<ahref="https://coder.com/docs"target="_blank"rel="noreferrer"className={styles.link}>
88+
<MenuItemclassName={styles.menuItem}onClick={handleDropdownClick}>
89+
<ListItemIconclassName={styles.icon}>
90+
<DocsIcon/>
91+
</ListItemIcon>
92+
<ListItemTextprimary={Language.docsLabel}/>
93+
</MenuItem>
94+
</a>
6995

7096
<MenuItemclassName={styles.menuItem}onClick={onSignOut}>
7197
<ListItemIconclassName={styles.icon}>
7298
<LogoutIcon/>
7399
</ListItemIcon>
74-
<ListItemTextprimary="Sign Out"/>
100+
<ListItemTextprimary={Language.signOutLabel}/>
75101
</MenuItem>
76102
</div>
77103
</BorderedMenu>
@@ -84,6 +110,7 @@ export const useStyles = makeStyles((theme) => ({
84110
marginTop:theme.spacing(1),
85111
marginBottom:theme.spacing(1),
86112
},
113+
87114
inner:{
88115
display:"flex",
89116
alignItems:"center",
@@ -94,12 +121,14 @@ export const useStyles = makeStyles((theme) => ({
94121
userInfo:{
95122
marginBottom:theme.spacing(1),
96123
},
124+
97125
arrowIcon:{
98126
color:fade(theme.palette.primary.contrastText,0.7),
99127
marginLeft:theme.spacing(1),
100128
width:16,
101129
height:16,
102130
},
131+
103132
arrowIconUp:{
104133
color:theme.palette.primary.contrastText,
105134
},
@@ -114,6 +143,11 @@ export const useStyles = makeStyles((theme) => ({
114143
},
115144
},
116145

146+
link:{
147+
textDecoration:"none",
148+
color:"inherit",
149+
},
150+
117151
icon:{
118152
color:theme.palette.text.secondary,
119153
},

‎site/src/pages/preferences/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
importBoxfrom"@material-ui/core/Box"
2+
importPaperfrom"@material-ui/core/Paper"
3+
importReactfrom"react"
4+
import{Header}from"../../components/Header"
5+
import{Footer}from"../../components/Page"
6+
7+
exportconstPreferencesPage:React.FC=()=>{
8+
return(
9+
<Boxdisplay="flex"flexDirection="column">
10+
<Headertitle="Preferences"/>
11+
<Paperstyle={{maxWidth:"1380px",margin:"1em auto",width:"100%"}}>Preferences here!</Paper>
12+
<Footer/>
13+
</Box>
14+
)
15+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp