From e2b90ae91d306f25846fe1a8a03b6da60d2f694b Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 24 Jul 2024 11:33:29 +0200 Subject: [PATCH] fix: add workaround for tooltip (#7649) This PR adds the UI part of feature flag collaborators. Collaborators are hidden on windows smaller than size XL because we're not sure how to deal with them in those cases yet. --- .../GroupCardAvatars/NewGroupCardAvatars.tsx | 41 +++++--- .../common/UserAvatar/UserAvatar.tsx | 25 ++--- .../FeatureView/Collaborators.test.tsx | 14 +++ .../feature/FeatureView/Collaborators.tsx | 95 +++++++++++++++++++ .../feature/FeatureView/FeatureView.tsx | 22 ++++- .../AvatarCell.tsx | 10 +- frontend/src/interfaces/featureToggle.ts | 11 +++ src/lib/types/experimental.ts | 2 +- 8 files changed, 185 insertions(+), 35 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/Collaborators.test.tsx create mode 100644 frontend/src/component/feature/FeatureView/Collaborators.tsx diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx index adb8abfb4b..584c8065da 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx @@ -19,13 +19,14 @@ const StyledAvatars = styled('div')(({ theme }) => ({ marginLeft: theme.spacing(1), })); -const StyledAvatar = styled(UserAvatar)(({ theme }) => ({ - outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, - marginLeft: theme.spacing(-1), - '&:hover': { - outlineColor: theme.palette.primary.main, - }, -})); +const StyledAvatar = (component: typeof UserAvatar) => + styled(component)(({ theme }) => ({ + outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, + marginLeft: theme.spacing(-1), + '&:hover': { + outlineColor: theme.palette.primary.main, + }, + })); const StyledHeader = styled('h3')(({ theme }) => ({ margin: theme.spacing(0, 0, 1), @@ -42,13 +43,31 @@ interface IGroupCardAvatarsProps { }[]; header?: ReactNode; avatarLimit?: number; + AvatarComponent?: typeof UserAvatar; } export const GroupCardAvatars = ({ + AvatarComponent, + ...props +}: IGroupCardAvatarsProps) => { + const Avatar = StyledAvatar(AvatarComponent ?? UserAvatar); + + return ; +}; + +type GroupCardAvatarsInnerProps = Omit< + IGroupCardAvatarsProps, + 'AvatarComponent' +> & { + AvatarComponent: typeof UserAvatar; +}; + +const GroupCardAvatarsInner = ({ users = [], header = null, avatarLimit = 9, -}: IGroupCardAvatarsProps) => { + AvatarComponent, +}: GroupCardAvatarsInnerProps) => { const shownUsers = useMemo( () => users @@ -94,7 +113,7 @@ export const GroupCardAvatars = ({ /> {shownUsers.map((user) => ( - { @@ -107,9 +126,9 @@ export const GroupCardAvatars = ({ avatarLimit} show={ - + +{users.length - shownUsers.length} - + } /> prop !== 'avatarWidth', -})<{ avatarWidth?: (theme: Theme) => string }>(({ theme, avatarWidth }) => { - const width = avatarWidth ? avatarWidth(theme) : theme.spacing(3.5); - - return { - width, - height: width, - margin: 'auto', - backgroundColor: theme.palette.secondary.light, - color: theme.palette.text.primary, - fontSize: theme.fontSizes.smallerBody, - fontWeight: theme.fontWeight.bold, - }; -}); +const StyledAvatar = styled(Avatar)(({ theme }) => ({ + width: theme.spacing(3.5), + height: theme.spacing(3.5), + margin: 'auto', + backgroundColor: theme.palette.secondary.light, + color: theme.palette.text.primary, + fontSize: theme.fontSizes.smallerBody, + fontWeight: theme.fontWeight.bold, +})); export interface IUserAvatarProps extends AvatarProps { user?: Partial< @@ -35,7 +29,6 @@ export interface IUserAvatarProps extends AvatarProps { onMouseLeave?: () => void; className?: string; sx?: SxProps; - avatarWidth?: (theme: Theme) => string; hideTitle?: boolean; } diff --git a/frontend/src/component/feature/FeatureView/Collaborators.test.tsx b/frontend/src/component/feature/FeatureView/Collaborators.test.tsx new file mode 100644 index 0000000000..c47e07d690 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/Collaborators.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react'; +import { Collaborators } from './Collaborators'; + +test('renders nothing if collaborators is undefined', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); +}); + +test('renders nothing if users is empty', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); +}); diff --git a/frontend/src/component/feature/FeatureView/Collaborators.tsx b/frontend/src/component/feature/FeatureView/Collaborators.tsx new file mode 100644 index 0000000000..95cc6e92d3 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/Collaborators.tsx @@ -0,0 +1,95 @@ +import { styled } from '@mui/material'; +import { GroupCardAvatars } from 'component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars'; +import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; +import type { Collaborator } from 'interfaces/featureToggle'; +import type { FC } from 'react'; +import { Link } from 'react-router-dom'; + +const StyledAvatar = styled(UserAvatar)(({ theme }) => ({ + width: theme.spacing(3), + height: theme.spacing(3), +})); + +const LastModifiedByContainer = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateAreas: ` + 'description description' + 'avatar link' + `, + rowGap: theme.spacing(0.5), + columnGap: theme.spacing(1), + alignItems: 'center', + height: 'min-content', +})); + +const GridDescription = styled('span')({ gridArea: 'description' }); +const GridTooltip = styled(HtmlTooltip)({ gridArea: 'avatar' }); +const GridLink = styled(Link)({ gridArea: 'link' }); + +const LastModifiedBy: FC = ({ id, name, imageUrl }) => { + return ( + + Last modified by + + + + + + view change + + ); +}; + +const CollaboratorListContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexFlow: 'column', + gap: theme.spacing(0.5), + alignItems: 'flex-start', + height: 'min-content', +})); + +const CollaboratorList: FC<{ collaborators: Collaborator[] }> = ({ + collaborators, +}) => { + return ( + + Collaborators + + + ); +}; + +const Container = styled('article')(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(10), + alignItems: 'center', + justifyContent: 'space-between', + [theme.breakpoints.down('xl')]: { + display: 'none', + }, +})); + +type Props = { + collaborators: Collaborator[] | undefined; +}; + +export const Collaborators: FC = ({ collaborators }) => { + if (!collaborators || collaborators.length === 0) { + return null; + } + + const lastModifiedBy = collaborators[0]; + + return ( + + + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index 94430d25c6..485f27e44d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -50,6 +50,7 @@ import copy from 'copy-to-clipboard'; import useToast from 'hooks/useToast'; import { useUiFlag } from 'hooks/useUiFlag'; import type { IFeatureToggle } from 'interfaces/featureToggle'; +import { Collaborators } from './Collaborators'; const StyledHeader = styled('div')(({ theme }) => ({ backgroundColor: theme.palette.background.paper, @@ -113,8 +114,12 @@ const StyledSeparator = styled('div')(({ theme }) => ({ height: '1px', })); -const StyledTabContainer = styled('div')(({ theme }) => ({ - padding: theme.spacing(0, 4), +const StyledTabRow = styled('div')(({ theme }) => ({ + display: 'flex', + flexFlow: 'row nowrap', + gap: theme.spacing(4), + paddingInline: theme.spacing(4), + justifyContent: 'space-between', })); const StyledTabButton = styled(Tab)(({ theme }) => ({ @@ -155,6 +160,7 @@ export const FeatureView = () => { const [openStaleDialog, setOpenStaleDialog] = useState(false); const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false); const smallScreen = useMediaQuery(`(max-width:${500}px)`); + const showCollaborators = useUiFlag('featureCollaborators'); const { feature, loading, error, status } = useFeature( projectId, @@ -355,7 +361,7 @@ export const FeatureView = () => { - + { /> ))} - + + } + /> + } /> diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx index d299ed68dd..4f9fcc5ede 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx @@ -1,4 +1,4 @@ -import { type Theme, styled } from '@mui/material'; +import { styled } from '@mui/material'; import type { FC } from 'react'; import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly'; import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; @@ -35,6 +35,11 @@ const StyledSecondaryText = styled('p')(({ theme }) => ({ color: theme.palette.text.secondary, })); +const StyledAvatar = styled(UserAvatar)(({ theme }) => ({ + width: theme.spacing(3), + height: theme.spacing(3), +})); + export const AvatarCell = (onAvatarClick: (userId: number) => void): FC => ({ row: { original } }) => { @@ -67,14 +72,13 @@ export const AvatarCell = - theme.spacing(3)} /> diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index 3b97ce95d7..205cfcd366 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -38,6 +38,16 @@ export type Lifecycle = { enteredStageAt: string; }; +export type Collaborator = { + id: number; + name: string; + imageUrl: string; +}; + +export type CollaboratorData = { + users: Collaborator[]; +}; + /** * @deprecated use FeatureSchema from openapi */ @@ -65,6 +75,7 @@ export interface IFeatureToggle { name: string; imageUrl: string; }; + collaborators?: CollaboratorData; } export interface IDependency { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 09c5c630d0..c9e4705945 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -331,7 +331,7 @@ const flags: IFlags = { false, ), featureCollaborators: parseEnvVarBoolean( - process.env.UNEASH_EXPERIMENTAL_FEATURE_COLLABORATORS, + process.env.UNLEASH_EXPERIMENTAL_FEATURE_COLLABORATORS, false, ), };