1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00

feat: filter by user when interacting with the avatar (#7347)

This PR lets you filter by flag creator by interacting with the user's
avatar.


Additionally, I've switched the custom popover for the standard tooltip
that we use elsewhere in the table. This gives the table a more cohesive
feel. As such, I have also deleted the component created in a previous
PR, because it's no longer in use anywhere.

It now looks like this (when tabbed to; notice the focus ring):


![image](https://github.com/Unleash/unleash/assets/17786332/d321d9df-0b17-49c3-bea7-89331df3f994)
This commit is contained in:
Thomas Heartman 2024-06-11 12:15:35 +02:00 committed by GitHub
parent a602768c57
commit 3643016a0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 102 additions and 114 deletions

View File

@ -0,0 +1,14 @@
import { styled } from '@mui/material';
// Visually hide content, but make it available to screen readers
export const ScreenReaderOnly = styled('div')(() => ({
border: 0,
clip: 'rect(0 0 0 0)',
height: 'auto',
margin: 0,
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap',
}));

View File

@ -36,6 +36,7 @@ export interface IUserAvatarProps extends AvatarProps {
className?: string;
sx?: SxProps<Theme>;
avatarWidth?: (theme: Theme) => string;
hideTitle?: boolean;
}
export const UserAvatar: FC<IUserAvatarProps> = ({
@ -47,9 +48,10 @@ export const UserAvatar: FC<IUserAvatarProps> = ({
className,
sx,
children,
hideTitle,
...props
}) => {
if (!title && !onMouseEnter && user) {
if (!hideTitle && !title && !onMouseEnter && user) {
title = `${user?.name || user?.email || user?.username} (id: ${
user?.id
})`;

View File

@ -1,92 +0,0 @@
import { styled, Popover } from '@mui/material';
import { useState, type FC } from 'react';
import type React from 'react';
import { type IUserAvatarProps, UserAvatar } from './UserAvatar';
type PopoverProps = {
mainText: string;
open: boolean;
anchorEl: HTMLElement | null;
onPopoverClose(event: React.MouseEvent<HTMLElement>): void;
};
const StyledPopover = styled(Popover)(({ theme }) => ({
pointerEvents: 'none',
'.MuiPaper-root': {
padding: theme.spacing(1),
},
}));
const AvatarPopover = ({
mainText,
open,
anchorEl,
onPopoverClose,
}: PopoverProps) => {
return (
<StyledPopover
open={open}
anchorEl={anchorEl}
onClose={onPopoverClose}
disableScrollLock={true}
disableRestoreFocus={true}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<p>{mainText}</p>
</StyledPopover>
);
};
type UserAvatarWithPopoverProps = Omit<
IUserAvatarProps,
'user' | 'onMouseEnter' | 'onMouseLeave'
> & {
user: {
name: string;
id?: number;
imageUrl?: string;
};
};
export const UserAvatarWithPopover: FC<UserAvatarWithPopoverProps> = ({
user,
...userAvatarProps
}) => {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const onPopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const onPopoverClose = () => {
setAnchorEl(null);
};
const avatarOpen = Boolean(anchorEl);
return (
<>
<UserAvatar
{...userAvatarProps}
user={user}
data-loading
onMouseEnter={(event) => {
onPopoverOpen(event);
}}
onMouseLeave={onPopoverClose}
/>
<AvatarPopover
mainText={user.name}
open={avatarOpen}
anchorEl={anchorEl}
onPopoverClose={onPopoverClose}
/>
</>
);
};

View File

@ -0,0 +1,73 @@
import { type Theme, styled } from '@mui/material';
import type { FC } from 'react';
import { visuallyHiddenStyles } from '../CreateProject/NewCreateProjectForm/ConfigButtons/shared.styles';
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
type AvatarCellProps = {
row: {
original: {
createdBy: {
id: number;
name: string;
imageUrl?: string;
};
};
};
};
const StyledContainer = styled('div')({
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
const StyledAvatarButton = styled('button')({
border: 'none',
background: 'none',
cursor: 'pointer',
borderRadius: '100%',
padding: 0,
});
export const VisuallyHiddenButtonText = styled('span')(() => ({
...visuallyHiddenStyles,
position: 'absolute',
}));
export const AvatarCell =
(onAvatarClick: (userId: number) => void): FC<AvatarCellProps> =>
({ row: { original } }) => {
return (
<StyledContainer>
<HtmlTooltip
arrow
describeChild
title={original.createdBy.name}
>
<StyledAvatarButton
onClick={() => onAvatarClick(original.createdBy.id)}
>
<ScreenReaderOnly>
<span>
Show only flags created by{' '}
{original.createdBy.name}
</span>
</ScreenReaderOnly>
<UserAvatar
hideTitle
user={{
id: original.createdBy.id,
name: original.createdBy.name,
imageUrl: original.createdBy.imageUrl,
}}
avatarWidth={(theme: Theme) => theme.spacing(3)}
/>
</StyledAvatarButton>
</HtmlTooltip>
</StyledContainer>
);
};

View File

@ -39,8 +39,7 @@ import {
useProjectFeatureSearch,
useProjectFeatureSearchActions,
} from './useProjectFeatureSearch';
import { UserAvatarWithPopover } from '../../../common/UserAvatar/UserAvatarWithPopover';
import type { Theme } from '@mui/material';
import { AvatarCell } from './AvatarCell';
interface IPaginatedProjectFeatureTogglesProps {
environments: string[];
@ -69,10 +68,8 @@ export const ProjectFeatureToggles = ({
setTableState,
} = useProjectFeatureSearch(projectId);
const { onFlagTypeClick, onTagClick } = useProjectFeatureSearchActions(
tableState,
setTableState,
);
const { onFlagTypeClick, onTagClick, onAvatarClick } =
useProjectFeatureSearchActions(tableState, setTableState);
const filterState = {
tag: tableState.tag,
@ -175,20 +172,7 @@ export const ProjectFeatureToggles = ({
columnHelper.accessor('createdBy', {
id: 'createdBy',
header: 'By',
cell: ({ row: { original } }) => {
return (
<UserAvatarWithPopover
user={{
id: original.createdBy.id,
name: original.createdBy.name,
imageUrl: original.createdBy.imageUrl,
}}
avatarWidth={(theme: Theme) =>
theme.spacing(3)
}
/>
);
},
cell: AvatarCell(onAvatarClick),
enableSorting: false,
meta: {
width: '1%',

View File

@ -19,7 +19,8 @@ import { useUiFlag } from 'hooks/useUiFlag';
type Attribute =
| { key: 'tag'; operator: 'INCLUDE' }
| { key: 'type'; operator: 'IS' };
| { key: 'type'; operator: 'IS' }
| { key: 'createdBy'; operator: 'IS' };
export const useProjectFeatureSearch = (
projectId: string,
@ -102,9 +103,15 @@ export const useProjectFeatureSearchActions = (
onAttributeClick({ key: 'tag', operator: 'INCLUDE' }, tag);
const onFlagTypeClick = (type: string) =>
onAttributeClick({ key: 'type', operator: 'IS' }, type);
const onAvatarClick = (userId: number) =>
onAttributeClick(
{ key: 'createdBy', operator: 'IS' },
userId.toString(),
);
return {
onFlagTypeClick,
onTagClick,
onAvatarClick,
};
};