Skip to content

Commit 175be62

Browse files
refactor: Improve roles UI (#5576)
1 parent de0601d commit 175be62

File tree

11 files changed

+330
-232
lines changed

11 files changed

+330
-232
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import {
3+
MockOwnerRole,
4+
MockSiteRoles,
5+
MockUserAdminRole,
6+
} from "testHelpers/entities"
7+
import { EditRolesButtonProps, EditRolesButton } from "./EditRolesButton"
8+
9+
export default {
10+
title: "components/EditRolesButton",
11+
component: EditRolesButton,
12+
argTypes: {
13+
defaultIsOpen: {
14+
defaultValue: true,
15+
},
16+
},
17+
} as ComponentMeta<typeof EditRolesButton>
18+
19+
const Template: Story<EditRolesButtonProps> = (args) => (
20+
<EditRolesButton {...args} />
21+
)
22+
23+
export const Open = Template.bind({})
24+
Open.args = {
25+
roles: MockSiteRoles,
26+
selectedRoles: [MockUserAdminRole, MockOwnerRole],
27+
}
28+
Open.parameters = {
29+
chromatic: { delay: 300 },
30+
}
31+
32+
export const Loading = Template.bind({})
33+
Loading.args = {
34+
isLoading: true,
35+
roles: MockSiteRoles,
36+
selectedRoles: [MockUserAdminRole, MockOwnerRole],
37+
}
38+
Loading.parameters = {
39+
chromatic: { delay: 300 },
40+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import IconButton from "@material-ui/core/IconButton"
2+
import { EditSquare } from "components/Icons/EditSquare"
3+
import { useRef, useState, FC } from "react"
4+
import { makeStyles } from "@material-ui/core/styles"
5+
import { useTranslation } from "react-i18next"
6+
import Popover from "@material-ui/core/Popover"
7+
import { Stack } from "components/Stack/Stack"
8+
import Checkbox from "@material-ui/core/Checkbox"
9+
import UserIcon from "@material-ui/icons/PersonOutline"
10+
import { Role } from "api/typesGenerated"
11+
12+
const Option: React.FC<{
13+
value: string
14+
name: string
15+
description: string
16+
isChecked: boolean
17+
onChange: (roleName: string) => void
18+
}> = ({ value, name, description, isChecked, onChange }) => {
19+
const styles = useStyles()
20+
21+
return (
22+
<label htmlFor={name} className={styles.option}>
23+
<Stack direction="row" alignItems="flex-start">
24+
<Checkbox
25+
id={name}
26+
size="small"
27+
color="primary"
28+
className={styles.checkbox}
29+
value={value}
30+
checked={isChecked}
31+
onChange={(e) => {
32+
onChange(e.currentTarget.value)
33+
}}
34+
/>
35+
<Stack spacing={0.5}>
36+
<strong>{name}</strong>
37+
<span className={styles.optionDescription}>{description}</span>
38+
</Stack>
39+
</Stack>
40+
</label>
41+
)
42+
}
43+
44+
export interface EditRolesButtonProps {
45+
isLoading: boolean
46+
roles: Role[]
47+
selectedRoles: Role[]
48+
onChange: (roles: Role["name"][]) => void
49+
defaultIsOpen?: boolean
50+
}
51+
52+
export const EditRolesButton: FC<EditRolesButtonProps> = ({
53+
roles,
54+
selectedRoles,
55+
onChange,
56+
isLoading,
57+
defaultIsOpen = false,
58+
}) => {
59+
const styles = useStyles()
60+
const { t } = useTranslation("usersPage")
61+
const anchorRef = useRef<HTMLButtonElement>(null)
62+
const [isOpen, setIsOpen] = useState(defaultIsOpen)
63+
const id = isOpen ? "edit-roles-popover" : undefined
64+
const selectedRoleNames = selectedRoles.map((role) => role.name)
65+
66+
const handleChange = (roleName: string) => {
67+
if (selectedRoleNames.includes(roleName)) {
68+
onChange(selectedRoleNames.filter((role) => role !== roleName))
69+
return
70+
}
71+
72+
onChange([...selectedRoleNames, roleName])
73+
}
74+
75+
return (
76+
<>
77+
<IconButton
78+
ref={anchorRef}
79+
size="small"
80+
className={styles.editButton}
81+
title={t("editUserRolesTooltip")}
82+
onClick={() => setIsOpen(true)}
83+
>
84+
<EditSquare />
85+
</IconButton>
86+
87+
<Popover
88+
id={id}
89+
open={isOpen}
90+
anchorEl={anchorRef.current}
91+
onClose={() => setIsOpen(false)}
92+
anchorOrigin={{
93+
vertical: "bottom",
94+
horizontal: "left",
95+
}}
96+
transformOrigin={{
97+
vertical: "top",
98+
horizontal: "left",
99+
}}
100+
classes={{ paper: styles.popoverPaper }}
101+
>
102+
<fieldset
103+
className={styles.fieldset}
104+
disabled={isLoading}
105+
title={t("fieldSetRolesTooltip")}
106+
>
107+
<Stack className={styles.options} spacing={3}>
108+
{roles.map((role) => (
109+
<Option
110+
key={role.name}
111+
onChange={handleChange}
112+
isChecked={selectedRoleNames.includes(role.name)}
113+
value={role.name}
114+
name={role.display_name}
115+
description={t(`roleDescription.${role.name}`)}
116+
/>
117+
))}
118+
</Stack>
119+
</fieldset>
120+
<div className={styles.footer}>
121+
<Stack direction="row" alignItems="flex-start">
122+
<UserIcon className={styles.userIcon} />
123+
<Stack spacing={0.5}>
124+
<strong>{t("member")}</strong>
125+
<span className={styles.optionDescription}>
126+
{t("roleDescription.member")}
127+
</span>
128+
</Stack>
129+
</Stack>
130+
</div>
131+
</Popover>
132+
</>
133+
)
134+
}
135+
136+
const useStyles = makeStyles((theme) => ({
137+
editButton: {
138+
color: theme.palette.text.secondary,
139+
140+
"& .MuiSvgIcon-root": {
141+
width: theme.spacing(2),
142+
height: theme.spacing(2),
143+
position: "relative",
144+
top: -2, // Align the pencil square
145+
},
146+
147+
"&:hover": {
148+
color: theme.palette.text.primary,
149+
backgroundColor: "transparent",
150+
},
151+
},
152+
popoverPaper: {
153+
width: theme.spacing(45),
154+
marginTop: theme.spacing(1),
155+
background: theme.palette.background.paperLight,
156+
},
157+
fieldset: {
158+
border: 0,
159+
margin: 0,
160+
padding: 0,
161+
162+
"&:disabled": {
163+
opacity: 0.5,
164+
},
165+
},
166+
options: {
167+
padding: theme.spacing(3),
168+
},
169+
option: {
170+
cursor: "pointer",
171+
},
172+
checkbox: {
173+
padding: 0,
174+
position: "relative",
175+
top: 1, // Alignment
176+
177+
"& svg": {
178+
width: theme.spacing(2.5),
179+
height: theme.spacing(2.5),
180+
},
181+
},
182+
optionDescription: {
183+
fontSize: 12,
184+
color: theme.palette.text.secondary,
185+
},
186+
footer: {
187+
padding: theme.spacing(3),
188+
backgroundColor: theme.palette.background.paper,
189+
borderTop: `1px solid ${theme.palette.divider}`,
190+
},
191+
userIcon: {
192+
width: theme.spacing(2.5), // Same as the checkbox
193+
height: theme.spacing(2.5),
194+
color: theme.palette.primary.main,
195+
},
196+
}))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
2+
3+
export const EditSquare = (props: SvgIconProps): JSX.Element => (
4+
<SvgIcon {...props} viewBox="0 0 48 48">
5+
<path d="M9 47.4q-1.2 0-2.1-.9-.9-.9-.9-2.1v-30q0-1.2.9-2.1.9-.9 2.1-.9h20.25l-3 3H9v30h30V27l3-3v20.4q0 1.2-.9 2.1-.9.9-2.1.9Zm15-18Zm9.1-17.6 2.15 2.1L21 28.1v4.3h4.25l14.3-14.3 2.1 2.1L26.5 35.4H18v-8.5Zm8.55 8.4-8.55-8.4 5-5q.85-.85 2.125-.85t2.125.9l4.2 4.25q.85.9.85 2.125t-.9 2.075Z" />
6+
</SvgIcon>
7+
)

site/src/components/RoleSelect/RoleSelect.stories.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

site/src/components/RoleSelect/RoleSelect.test.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)