mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
fix(site): Minor UI fixes related to avatar components (#6019)
This commit is contained in:
@ -9,7 +9,7 @@ import { combineClasses } from "util/combineClasses"
|
||||
import { firstLetter } from "./firstLetter"
|
||||
|
||||
export type AvatarProps = MuiAvatarProps & {
|
||||
size?: "md" | "xl"
|
||||
size?: "sm" | "md" | "xl"
|
||||
colorScheme?: "light" | "darken"
|
||||
fitImage?: boolean
|
||||
}
|
||||
@ -50,6 +50,11 @@ export const AvatarIcon: FC<{ src: string }> = ({ src }) => {
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
// Size styles
|
||||
sm: {
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
fontSize: theme.spacing(1.5),
|
||||
},
|
||||
// Just use the default value from theme
|
||||
md: {},
|
||||
xl: {
|
||||
|
@ -156,6 +156,11 @@ interface StyleProps {
|
||||
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
||||
root: {
|
||||
marginBottom: theme.spacing(2),
|
||||
|
||||
"&:has(button) .MuiInputBase-root": {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
},
|
||||
// necessary to expand the textField
|
||||
// the length of the page (within the bordered filterContainer)
|
||||
@ -174,7 +179,6 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
||||
inputStyles: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
|
||||
@ -189,7 +193,7 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
||||
paddingTop: "inherit",
|
||||
paddingBottom: "inherit",
|
||||
// The same as the button
|
||||
minHeight: 42,
|
||||
minHeight: 40,
|
||||
},
|
||||
},
|
||||
searchIcon: {
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { MockUser } from "testHelpers/entities"
|
||||
import { UserAutocomplete, UserAutocompleteProps } from "./UserAutocomplete"
|
||||
|
||||
export default {
|
||||
title: "components/UserAutocomplete",
|
||||
component: UserAutocomplete,
|
||||
}
|
||||
|
||||
const Template: Story<UserAutocompleteProps> = (
|
||||
args: UserAutocompleteProps,
|
||||
) => <UserAutocomplete {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
value: MockUser,
|
||||
label: "User",
|
||||
}
|
||||
|
||||
export const NoLabel = Template.bind({})
|
||||
NoLabel.args = {
|
||||
value: MockUser,
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import CircularProgress from "@material-ui/core/CircularProgress"
|
||||
import { makeStyles, Theme } from "@material-ui/core/styles"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete"
|
||||
import { useMachine } from "@xstate/react"
|
||||
@ -8,29 +8,22 @@ import { Avatar } from "components/Avatar/Avatar"
|
||||
import { AvatarData } from "components/AvatarData/AvatarData"
|
||||
import debounce from "just-debounce-it"
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
import { searchUserMachine } from "xServices/users/searchUserXService"
|
||||
|
||||
export type UserAutocompleteProps = {
|
||||
value: User | null
|
||||
onChange: (user: User | null) => void
|
||||
label?: string
|
||||
inputMargin?: "none" | "dense" | "normal"
|
||||
inputStyles?: string
|
||||
className?: string
|
||||
showAvatar?: boolean
|
||||
}
|
||||
|
||||
export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
label,
|
||||
inputMargin,
|
||||
inputStyles,
|
||||
showAvatar = false,
|
||||
className,
|
||||
}) => {
|
||||
const styles = useStyles({ showAvatar })
|
||||
const styles = useStyles()
|
||||
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false)
|
||||
const [searchState, sendSearch] = useMachine(searchUserMachine)
|
||||
const { searchResults } = searchState.context
|
||||
@ -53,6 +46,9 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
className={className}
|
||||
options={searchResults}
|
||||
loading={searchState.matches("searching")}
|
||||
value={value}
|
||||
id="user-autocomplete"
|
||||
open={isAutocompleteOpen}
|
||||
@ -80,22 +76,21 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
src={option.avatar_url}
|
||||
/>
|
||||
)}
|
||||
options={searchResults}
|
||||
loading={searchState.matches("searching")}
|
||||
className={combineClasses([styles.autocomplete, className])}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin={inputMargin ?? "normal"}
|
||||
label={label ?? undefined}
|
||||
placeholder="User email or username"
|
||||
className={inputStyles}
|
||||
className={styles.textField}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
onChange: handleFilterChange,
|
||||
startAdornment: showAvatar && value && (
|
||||
<Avatar src={value.avatar_url}>{value.username}</Avatar>
|
||||
startAdornment: value && (
|
||||
<Avatar size="sm" src={value.avatar_url}>
|
||||
{value.username}
|
||||
</Avatar>
|
||||
),
|
||||
endAdornment: (
|
||||
<>
|
||||
@ -105,6 +100,9 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
),
|
||||
classes: {
|
||||
root: styles.inputRoot,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -112,54 +110,14 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
interface styleProps {
|
||||
showAvatar: boolean
|
||||
}
|
||||
|
||||
export const useStyles = makeStyles<Theme, styleProps>((theme) => {
|
||||
return {
|
||||
autocomplete: (props) => ({
|
||||
width: "100%",
|
||||
|
||||
"& .MuiFormControl-root": {
|
||||
width: "100%",
|
||||
},
|
||||
|
||||
"& .MuiInputBase-root": {
|
||||
width: "100%",
|
||||
// Match button small height
|
||||
height: props.showAvatar ? 60 : 40,
|
||||
},
|
||||
|
||||
"& input": {
|
||||
fontSize: 16,
|
||||
padding: `${theme.spacing(0, 0.5, 0, 0.5)} !important`,
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
export const UserAutocompleteInline: React.FC<UserAutocompleteProps> = (
|
||||
props,
|
||||
) => {
|
||||
const style = useInlineStyle()
|
||||
|
||||
return <UserAutocomplete {...props} className={style.inline} />
|
||||
}
|
||||
|
||||
export const useInlineStyle = makeStyles(() => {
|
||||
return {
|
||||
inline: {
|
||||
width: "300px",
|
||||
|
||||
"& .MuiFormControl-root": {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
"& .MuiInputBase-root": {
|
||||
// Match button small height
|
||||
height: 36,
|
||||
},
|
||||
export const useStyles = makeStyles((theme) => ({
|
||||
textField: {
|
||||
"&:not(:has(label))": {
|
||||
margin: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
inputRoot: {
|
||||
paddingLeft: `${theme.spacing(1.75)}px !important`, // Same padding left as input
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
}))
|
||||
|
@ -92,8 +92,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
const styles = useStyles()
|
||||
const navigate = useNavigate()
|
||||
const serverVersion = buildInfo?.version || ""
|
||||
const hasTemplateIcon =
|
||||
workspace.template_icon && workspace.template_icon !== ""
|
||||
|
||||
const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && (
|
||||
<AlertBanner
|
||||
@ -159,14 +157,14 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
}
|
||||
>
|
||||
<Stack direction="row" spacing={3} alignItems="center">
|
||||
{hasTemplateIcon && (
|
||||
<Avatar
|
||||
size="xl"
|
||||
src={workspace.template_icon}
|
||||
variant="square"
|
||||
fitImage
|
||||
/>
|
||||
)}
|
||||
<Avatar
|
||||
size="xl"
|
||||
src={workspace.template_icon}
|
||||
variant={workspace.template_icon ? "square" : undefined}
|
||||
fitImage={Boolean(workspace.template_icon)}
|
||||
>
|
||||
{workspace.name}
|
||||
</Avatar>
|
||||
<div>
|
||||
<PageHeaderTitle>
|
||||
{workspace.name}
|
||||
|
@ -4,7 +4,6 @@ import TableRow from "@material-ui/core/TableRow"
|
||||
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"
|
||||
import { AvatarData } from "components/AvatarData/AvatarData"
|
||||
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
|
||||
import { useClickable } from "hooks/useClickable"
|
||||
import { FC } from "react"
|
||||
import { useNavigate, Link as RouterLink } from "react-router-dom"
|
||||
import { getDisplayWorkspaceTemplateName } from "util/workspace"
|
||||
@ -15,6 +14,7 @@ import { Avatar } from "components/Avatar/Avatar"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import TemplateLinkIcon from "@material-ui/icons/OpenInNewOutlined"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { useClickableTableRow } from "hooks/useClickableTableRow"
|
||||
|
||||
export const WorkspacesRow: FC<{
|
||||
workspace: Workspace
|
||||
@ -23,20 +23,13 @@ export const WorkspacesRow: FC<{
|
||||
const styles = useStyles()
|
||||
const navigate = useNavigate()
|
||||
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`
|
||||
const hasTemplateIcon =
|
||||
workspace.template_icon && workspace.template_icon !== ""
|
||||
const displayTemplateName = getDisplayWorkspaceTemplateName(workspace)
|
||||
const clickable = useClickable(() => {
|
||||
const clickable = useClickableTableRow(() => {
|
||||
navigate(workspacePageLink)
|
||||
})
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className={styles.row}
|
||||
hover
|
||||
data-testid={`workspace-${workspace.id}`}
|
||||
{...clickable}
|
||||
>
|
||||
<TableRow data-testid={`workspace-${workspace.id}`} {...clickable}>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
title={
|
||||
@ -53,9 +46,13 @@ export const WorkspacesRow: FC<{
|
||||
}
|
||||
subtitle={workspace.owner_name}
|
||||
avatar={
|
||||
hasTemplateIcon && (
|
||||
<Avatar src={workspace.template_icon} variant="square" fitImage />
|
||||
)
|
||||
<Avatar
|
||||
src={workspace.template_icon}
|
||||
variant={workspace.template_icon ? "square" : undefined}
|
||||
fitImage={Boolean(workspace.template_icon)}
|
||||
>
|
||||
{workspace.name}
|
||||
</Avatar>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
@ -95,15 +92,6 @@ export const WorkspacesRow: FC<{
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
row: {
|
||||
cursor: "pointer",
|
||||
|
||||
"&:focus": {
|
||||
outline: `1px solid ${theme.palette.secondary.dark}`,
|
||||
outlineOffset: -1,
|
||||
},
|
||||
},
|
||||
|
||||
arrowRight: {
|
||||
color: theme.palette.text.secondary,
|
||||
width: 20,
|
||||
|
@ -27,5 +27,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
outline: `1px solid ${theme.palette.secondary.dark}`,
|
||||
outlineOffset: -1,
|
||||
},
|
||||
|
||||
"&:last-of-type": {
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomRightRadius: theme.shape.borderRadius,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
@ -215,8 +215,6 @@ export const CreateWorkspacePageView: FC<
|
||||
value={props.owner}
|
||||
onChange={props.setOwner}
|
||||
label={t("ownerLabel")}
|
||||
inputMargin="dense"
|
||||
showAvatar
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
|
@ -13,7 +13,10 @@ const Template: Story<SelectedTemplateProps> = (args) => (
|
||||
|
||||
export const WithIcon = Template.bind({})
|
||||
WithIcon.args = {
|
||||
template: MockTemplate,
|
||||
template: {
|
||||
...MockTemplate,
|
||||
icon: "/icon/docker.png",
|
||||
},
|
||||
}
|
||||
|
||||
export const WithoutIcon = Template.bind({})
|
||||
|
@ -18,7 +18,13 @@ export const SelectedTemplate: FC<SelectedTemplateProps> = ({ template }) => {
|
||||
className={styles.template}
|
||||
alignItems="center"
|
||||
>
|
||||
<Avatar src={template.icon}>{template.name}</Avatar>
|
||||
<Avatar
|
||||
variant={template.icon ? "square" : undefined}
|
||||
fitImage={Boolean(template.icon)}
|
||||
src={template.icon}
|
||||
>
|
||||
{template.name}
|
||||
</Avatar>
|
||||
|
||||
<Stack direction="column" spacing={0.5}>
|
||||
<span className={styles.templateName}>
|
||||
|
@ -25,19 +25,21 @@ import {
|
||||
} from "components/PageHeader/PageHeader"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { TableRowMenu } from "components/TableRowMenu/TableRowMenu"
|
||||
import { UserAutocompleteInline } from "components/UserAutocomplete/UserAutocomplete"
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
||||
import { useState } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"
|
||||
import { pageTitle } from "util/page"
|
||||
import { groupMachine } from "xServices/groups/groupXService"
|
||||
import { Maybe } from "components/Conditionals/Maybe"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
|
||||
const AddGroupMember: React.FC<{
|
||||
isLoading: boolean
|
||||
onSubmit: (user: User, reset: () => void) => void
|
||||
}> = ({ isLoading, onSubmit }) => {
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
||||
const styles = useStyles()
|
||||
|
||||
const resetValues = () => {
|
||||
setSelectedUser(null)
|
||||
@ -54,7 +56,8 @@ const AddGroupMember: React.FC<{
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<UserAutocompleteInline
|
||||
<UserAutocomplete
|
||||
className={styles.autoComplete}
|
||||
value={selectedUser}
|
||||
onChange={(newValue) => {
|
||||
setSelectedUser(newValue)
|
||||
@ -64,7 +67,6 @@ const AddGroupMember: React.FC<{
|
||||
<LoadingButton
|
||||
disabled={!selectedUser}
|
||||
type="submit"
|
||||
size="small"
|
||||
startIcon={<PersonAdd />}
|
||||
loading={isLoading}
|
||||
>
|
||||
@ -223,4 +225,10 @@ export const GroupPage: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
autoComplete: {
|
||||
width: 300,
|
||||
},
|
||||
}))
|
||||
|
||||
export default GroupPage
|
||||
|
@ -42,7 +42,8 @@ export const getOverrides = ({
|
||||
MuiButton: {
|
||||
root: {
|
||||
// Prevents a loading button from collapsing!
|
||||
minHeight: 42,
|
||||
minHeight: 40,
|
||||
height: 40, // Same size of input height
|
||||
fontWeight: "normal",
|
||||
fontSize: 16,
|
||||
textTransform: "none",
|
||||
@ -73,6 +74,7 @@ export const getOverrides = ({
|
||||
padding: `0 16px`,
|
||||
fontSize: 14,
|
||||
minHeight: 36,
|
||||
height: 36,
|
||||
borderRadius: borderRadiusSm,
|
||||
},
|
||||
iconSizeSmall: {
|
||||
|
Reference in New Issue
Block a user