mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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): 
This commit is contained in:
		
							parent
							
								
									a602768c57
								
							
						
					
					
						commit
						3643016a0e
					
				@ -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',
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
@ -36,6 +36,7 @@ export interface IUserAvatarProps extends AvatarProps {
 | 
				
			|||||||
    className?: string;
 | 
					    className?: string;
 | 
				
			||||||
    sx?: SxProps<Theme>;
 | 
					    sx?: SxProps<Theme>;
 | 
				
			||||||
    avatarWidth?: (theme: Theme) => string;
 | 
					    avatarWidth?: (theme: Theme) => string;
 | 
				
			||||||
 | 
					    hideTitle?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const UserAvatar: FC<IUserAvatarProps> = ({
 | 
					export const UserAvatar: FC<IUserAvatarProps> = ({
 | 
				
			||||||
@ -47,9 +48,10 @@ export const UserAvatar: FC<IUserAvatarProps> = ({
 | 
				
			|||||||
    className,
 | 
					    className,
 | 
				
			||||||
    sx,
 | 
					    sx,
 | 
				
			||||||
    children,
 | 
					    children,
 | 
				
			||||||
 | 
					    hideTitle,
 | 
				
			||||||
    ...props
 | 
					    ...props
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    if (!title && !onMouseEnter && user) {
 | 
					    if (!hideTitle && !title && !onMouseEnter && user) {
 | 
				
			||||||
        title = `${user?.name || user?.email || user?.username} (id: ${
 | 
					        title = `${user?.name || user?.email || user?.username} (id: ${
 | 
				
			||||||
            user?.id
 | 
					            user?.id
 | 
				
			||||||
        })`;
 | 
					        })`;
 | 
				
			||||||
 | 
				
			|||||||
@ -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}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
        </>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@ -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>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
@ -39,8 +39,7 @@ import {
 | 
				
			|||||||
    useProjectFeatureSearch,
 | 
					    useProjectFeatureSearch,
 | 
				
			||||||
    useProjectFeatureSearchActions,
 | 
					    useProjectFeatureSearchActions,
 | 
				
			||||||
} from './useProjectFeatureSearch';
 | 
					} from './useProjectFeatureSearch';
 | 
				
			||||||
import { UserAvatarWithPopover } from '../../../common/UserAvatar/UserAvatarWithPopover';
 | 
					import { AvatarCell } from './AvatarCell';
 | 
				
			||||||
import type { Theme } from '@mui/material';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IPaginatedProjectFeatureTogglesProps {
 | 
					interface IPaginatedProjectFeatureTogglesProps {
 | 
				
			||||||
    environments: string[];
 | 
					    environments: string[];
 | 
				
			||||||
@ -69,10 +68,8 @@ export const ProjectFeatureToggles = ({
 | 
				
			|||||||
        setTableState,
 | 
					        setTableState,
 | 
				
			||||||
    } = useProjectFeatureSearch(projectId);
 | 
					    } = useProjectFeatureSearch(projectId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { onFlagTypeClick, onTagClick } = useProjectFeatureSearchActions(
 | 
					    const { onFlagTypeClick, onTagClick, onAvatarClick } =
 | 
				
			||||||
        tableState,
 | 
					        useProjectFeatureSearchActions(tableState, setTableState);
 | 
				
			||||||
        setTableState,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const filterState = {
 | 
					    const filterState = {
 | 
				
			||||||
        tag: tableState.tag,
 | 
					        tag: tableState.tag,
 | 
				
			||||||
@ -175,20 +172,7 @@ export const ProjectFeatureToggles = ({
 | 
				
			|||||||
                      columnHelper.accessor('createdBy', {
 | 
					                      columnHelper.accessor('createdBy', {
 | 
				
			||||||
                          id: 'createdBy',
 | 
					                          id: 'createdBy',
 | 
				
			||||||
                          header: 'By',
 | 
					                          header: 'By',
 | 
				
			||||||
                          cell: ({ row: { original } }) => {
 | 
					                          cell: AvatarCell(onAvatarClick),
 | 
				
			||||||
                              return (
 | 
					 | 
				
			||||||
                                  <UserAvatarWithPopover
 | 
					 | 
				
			||||||
                                      user={{
 | 
					 | 
				
			||||||
                                          id: original.createdBy.id,
 | 
					 | 
				
			||||||
                                          name: original.createdBy.name,
 | 
					 | 
				
			||||||
                                          imageUrl: original.createdBy.imageUrl,
 | 
					 | 
				
			||||||
                                      }}
 | 
					 | 
				
			||||||
                                      avatarWidth={(theme: Theme) =>
 | 
					 | 
				
			||||||
                                          theme.spacing(3)
 | 
					 | 
				
			||||||
                                      }
 | 
					 | 
				
			||||||
                                  />
 | 
					 | 
				
			||||||
                              );
 | 
					 | 
				
			||||||
                          },
 | 
					 | 
				
			||||||
                          enableSorting: false,
 | 
					                          enableSorting: false,
 | 
				
			||||||
                          meta: {
 | 
					                          meta: {
 | 
				
			||||||
                              width: '1%',
 | 
					                              width: '1%',
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,8 @@ import { useUiFlag } from 'hooks/useUiFlag';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type Attribute =
 | 
					type Attribute =
 | 
				
			||||||
    | { key: 'tag'; operator: 'INCLUDE' }
 | 
					    | { key: 'tag'; operator: 'INCLUDE' }
 | 
				
			||||||
    | { key: 'type'; operator: 'IS' };
 | 
					    | { key: 'type'; operator: 'IS' }
 | 
				
			||||||
 | 
					    | { key: 'createdBy'; operator: 'IS' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useProjectFeatureSearch = (
 | 
					export const useProjectFeatureSearch = (
 | 
				
			||||||
    projectId: string,
 | 
					    projectId: string,
 | 
				
			||||||
@ -102,9 +103,15 @@ export const useProjectFeatureSearchActions = (
 | 
				
			|||||||
        onAttributeClick({ key: 'tag', operator: 'INCLUDE' }, tag);
 | 
					        onAttributeClick({ key: 'tag', operator: 'INCLUDE' }, tag);
 | 
				
			||||||
    const onFlagTypeClick = (type: string) =>
 | 
					    const onFlagTypeClick = (type: string) =>
 | 
				
			||||||
        onAttributeClick({ key: 'type', operator: 'IS' }, type);
 | 
					        onAttributeClick({ key: 'type', operator: 'IS' }, type);
 | 
				
			||||||
 | 
					    const onAvatarClick = (userId: number) =>
 | 
				
			||||||
 | 
					        onAttributeClick(
 | 
				
			||||||
 | 
					            { key: 'createdBy', operator: 'IS' },
 | 
				
			||||||
 | 
					            userId.toString(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        onFlagTypeClick,
 | 
					        onFlagTypeClick,
 | 
				
			||||||
        onTagClick,
 | 
					        onTagClick,
 | 
				
			||||||
 | 
					        onAvatarClick,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user