mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-11 00:08:30 +01:00
chore: group cards redesign (#9048)
https://linear.app/unleash/issue/2-3108/cards-design-groups Redesigns the group cards. Like instructed in the task, I took inspiration from the project and integration cards, along with the Figma sketch. Also includes a new `Truncator` generic helper component. ### Before ![image](https://github.com/user-attachments/assets/e47ebb3d-a089-4cbb-962c-53af9f1933f9) ### After ![image](https://github.com/user-attachments/assets/ffeb96b7-e6c4-4433-a847-2e267beb72e9) Hovering over the "X projects" label reveals the projects the group belongs to. You can navigate to any project by clicking its badge. ![image](https://github.com/user-attachments/assets/cf06c7f5-011e-4b89-8e40-ed42e5817625) Truncated titles and descriptions show a tooltip with the full text on hover. ![image](https://github.com/user-attachments/assets/6fc598e7-b08a-4bfa-8cb2-4153a81f2a48) ![image](https://github.com/user-attachments/assets/91ceba73-c43e-4070-9de0-2a182a3d9257)
This commit is contained in:
parent
54d6bd5b86
commit
67c1274a1b
@ -1,4 +1,4 @@
|
||||
import { styled, Tooltip } from '@mui/material';
|
||||
import { Box, Card, styled } from '@mui/material';
|
||||
import type { IGroup } from 'interfaces/group';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
@ -7,88 +7,119 @@ import { GroupCardActions } from './GroupCardActions/GroupCardActions';
|
||||
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
||||
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';
|
||||
import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
|
||||
import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup';
|
||||
import {
|
||||
AvatarComponent,
|
||||
AvatarGroup,
|
||||
} from 'component/common/AvatarGroup/AvatarGroup';
|
||||
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
import { Truncator } from 'component/common/Truncator/Truncator';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
const StyledCardLink = styled(Link)(({ theme }) => ({
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
color: theme.palette.text.primary,
|
||||
border: 'none',
|
||||
padding: '0',
|
||||
background: 'transparent',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
pointer: 'cursor',
|
||||
}));
|
||||
|
||||
const StyledGroupCard = styled('aside')(({ theme }) => ({
|
||||
padding: theme.spacing(2.5),
|
||||
height: '100%',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
boxShadow: theme.boxShadows.card,
|
||||
const StyledCard = styled(Card)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
padding: theme.spacing(4),
|
||||
justifyContent: 'space-between',
|
||||
height: '100%',
|
||||
boxShadow: 'none',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
transition: 'background-color 0.2s ease-in-out',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
'&:hover': {
|
||||
transition: 'background-color 0.2s ease-in-out',
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
},
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
}));
|
||||
|
||||
const StyledRow = styled('div')(() => ({
|
||||
const StyledCardBody = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}));
|
||||
|
||||
const StyledCardBodyHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StyledTitleRow = styled(StyledRow)(() => ({
|
||||
alignItems: 'flex-start',
|
||||
const StyledCardIconContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
padding: theme.spacing(0.5),
|
||||
alignSelf: 'baseline',
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
color: theme.palette.primary.main,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
'& > svg': {
|
||||
height: theme.spacing(2),
|
||||
width: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledBottomRow = styled(StyledRow)(({ theme }) => ({
|
||||
marginTop: 'auto',
|
||||
alignItems: 'flex-end',
|
||||
const StyledCardTitle = styled('h3')(({ theme }) => ({
|
||||
margin: 0,
|
||||
marginRight: 'auto',
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
lineHeight: '1.2',
|
||||
}));
|
||||
|
||||
const StyledCardDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
marginTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledCardFooter = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(0, 2),
|
||||
display: 'flex',
|
||||
background: theme.palette.envAccordion.expanded,
|
||||
boxShadow: theme.boxShadows.accordionFooter,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
minHeight: theme.spacing(6.25),
|
||||
}));
|
||||
|
||||
const StyledCardFooterSpan = styled('span')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
color: theme.palette.text.secondary,
|
||||
textWrap: 'nowrap',
|
||||
}));
|
||||
|
||||
const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({
|
||||
height: theme.spacing(2.5),
|
||||
width: theme.spacing(2.5),
|
||||
marginLeft: theme.spacing(-0.75),
|
||||
}));
|
||||
|
||||
const StyledProjectsTooltip = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledHeaderTitle = styled('h2')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
}));
|
||||
|
||||
const StyledHeaderActions = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const StyledCounterDescription = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginLeft: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const ProjectBadgeContainer = styled('div')(({ theme }) => ({
|
||||
maxWidth: '50%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.spacing(0.5),
|
||||
flexWrap: 'wrap',
|
||||
}));
|
||||
|
||||
const InfoBadgeDescription = styled('span')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
color: theme.palette.text.secondary,
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const ProjectNameBadge = styled(Badge)({
|
||||
wordBreak: 'break-word',
|
||||
const StyledProjectBadge = styled(Badge)({
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
interface IGroupCardProps {
|
||||
@ -104,6 +135,8 @@ export const GroupCard = ({
|
||||
}: IGroupCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
const {
|
||||
settings: { enabled: scimEnabled },
|
||||
} = useScimSettings();
|
||||
@ -111,84 +144,104 @@ export const GroupCard = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledLink key={group.id} to={`/admin/groups/${group.id}`}>
|
||||
<StyledGroupCard>
|
||||
<StyledTitleRow>
|
||||
<StyledHeaderTitle>{group.name}</StyledHeaderTitle>
|
||||
<StyledHeaderActions>
|
||||
<StyledCardLink to={`/admin/groups/${group.id}`}>
|
||||
<StyledCard>
|
||||
<StyledCardBody>
|
||||
<StyledCardBodyHeader>
|
||||
<StyledCardIconContainer>
|
||||
<GroupsIcon />
|
||||
</StyledCardIconContainer>
|
||||
<Truncator
|
||||
title={group.name}
|
||||
arrow
|
||||
component={StyledCardTitle}
|
||||
>
|
||||
<Highlighter search={searchQuery}>
|
||||
{group.name}
|
||||
</Highlighter>
|
||||
</Truncator>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(group.rootRole)}
|
||||
show={
|
||||
<RoleBadge
|
||||
roleId={group.rootRole!}
|
||||
hideIcon
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<GroupCardActions
|
||||
groupId={group.id}
|
||||
onEditUsers={() => onEditUsers(group)}
|
||||
onRemove={() => onRemoveGroup(group)}
|
||||
isScimGroup={isScimGroup}
|
||||
/>
|
||||
</StyledHeaderActions>
|
||||
</StyledTitleRow>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(group.rootRole)}
|
||||
show={
|
||||
<InfoBadgeDescription>
|
||||
<p>Root role:</p>
|
||||
<RoleBadge roleId={group.rootRole!} />
|
||||
</InfoBadgeDescription>
|
||||
}
|
||||
/>
|
||||
|
||||
<StyledDescription>{group.description}</StyledDescription>
|
||||
<StyledBottomRow>
|
||||
</StyledCardBodyHeader>
|
||||
<ConditionallyRender
|
||||
condition={group.users?.length > 0}
|
||||
show={<AvatarGroup users={group.users} />}
|
||||
elseShow={
|
||||
<StyledCounterDescription>
|
||||
This group has no users.
|
||||
</StyledCounterDescription>
|
||||
condition={Boolean(group.description)}
|
||||
show={
|
||||
<Truncator
|
||||
lines={2}
|
||||
title={group.description}
|
||||
arrow
|
||||
component={StyledCardDescription}
|
||||
>
|
||||
<Highlighter search={searchQuery}>
|
||||
{group.description}
|
||||
</Highlighter>
|
||||
</Truncator>
|
||||
}
|
||||
/>
|
||||
<ProjectBadgeContainer>
|
||||
<ConditionallyRender
|
||||
condition={group.projects.length > 0}
|
||||
show={group.projects.map((project) => (
|
||||
<Tooltip
|
||||
key={project}
|
||||
title='View project'
|
||||
arrow
|
||||
placement='bottom-end'
|
||||
describeChild
|
||||
>
|
||||
<ProjectNameBadge
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(
|
||||
`/projects/${project}/settings/access`,
|
||||
);
|
||||
}}
|
||||
color='secondary'
|
||||
icon={<TopicOutlinedIcon />}
|
||||
>
|
||||
{project}
|
||||
</ProjectNameBadge>
|
||||
</Tooltip>
|
||||
))}
|
||||
elseShow={
|
||||
<ConditionallyRender
|
||||
condition={!group.rootRole}
|
||||
show={
|
||||
<Tooltip
|
||||
title='This group is not used in any project'
|
||||
arrow
|
||||
describeChild
|
||||
>
|
||||
<Badge>Not used</Badge>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ProjectBadgeContainer>
|
||||
</StyledBottomRow>
|
||||
</StyledGroupCard>
|
||||
</StyledLink>
|
||||
</StyledCardBody>
|
||||
<StyledCardFooter>
|
||||
<ConditionallyRender
|
||||
condition={group.users.length > 0}
|
||||
show={
|
||||
<AvatarGroup
|
||||
users={group.users}
|
||||
AvatarComponent={StyledAvatarComponent}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<StyledCardFooterSpan>
|
||||
This group has no users
|
||||
</StyledCardFooterSpan>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={group.projects.length > 0}
|
||||
show={
|
||||
<TooltipLink
|
||||
component='span'
|
||||
tooltip={
|
||||
<StyledProjectsTooltip>
|
||||
{group.projects.map((project) => (
|
||||
<StyledProjectBadge
|
||||
key={project}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(
|
||||
`/projects/${project}/settings/access`,
|
||||
);
|
||||
}}
|
||||
color='secondary'
|
||||
icon={<TopicOutlinedIcon />}
|
||||
>
|
||||
{project}
|
||||
</StyledProjectBadge>
|
||||
))}
|
||||
</StyledProjectsTooltip>
|
||||
}
|
||||
>
|
||||
<StyledCardFooterSpan>
|
||||
{group.projects.length} project
|
||||
{group.projects.length !== 1 && 's'}
|
||||
</StyledCardFooterSpan>
|
||||
</TooltipLink>
|
||||
}
|
||||
/>
|
||||
</StyledCardFooter>
|
||||
</StyledCard>
|
||||
</StyledCardLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -18,9 +18,11 @@ import { Link } from 'react-router-dom';
|
||||
import { scimGroupTooltip } from 'component/admin/groups/group-constants';
|
||||
|
||||
const StyledActions = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(-1),
|
||||
marginLeft: theme.spacing(-0.5),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
transform: 'translate3d(8px, -6px, 0)',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||
@ -51,7 +53,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const id = `feature-${groupId}-actions`;
|
||||
const id = `group-${groupId}-actions`;
|
||||
const menuId = `${id}-menu`;
|
||||
|
||||
return (
|
||||
@ -74,6 +76,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
type='button'
|
||||
size='small'
|
||||
>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
|
@ -6,7 +6,7 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { Grid, useMediaQuery } from '@mui/material';
|
||||
import { styled, useMediaQuery } from '@mui/material';
|
||||
import theme from 'themes/theme';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TablePlaceholder } from 'component/common/Table';
|
||||
@ -19,6 +19,12 @@ import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
|
||||
import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
|
||||
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
|
||||
|
||||
const StyledGridContainer = styled('div')(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
type PageQueryType = Partial<Record<'search', string>>;
|
||||
|
||||
const groupsSearch = (group: IGroup, searchValue: string) => {
|
||||
@ -131,17 +137,16 @@ export const GroupsList: VFC = () => {
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={searchValue}>
|
||||
<Grid container spacing={2}>
|
||||
<StyledGridContainer>
|
||||
{data.map((group) => (
|
||||
<Grid key={group.id} item xs={12} md={6}>
|
||||
<GroupCard
|
||||
group={group}
|
||||
onEditUsers={onEditUsers}
|
||||
onRemoveGroup={onRemoveGroup}
|
||||
/>
|
||||
</Grid>
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
onEditUsers={onEditUsers}
|
||||
onRemoveGroup={onRemoveGroup}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</StyledGridContainer>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={!loading && data.length === 0}
|
||||
|
@ -85,7 +85,7 @@ const AvatarGroupInner = ({
|
||||
return 0;
|
||||
})
|
||||
.slice(0, avatarLimit),
|
||||
[users],
|
||||
[users, avatarLimit],
|
||||
);
|
||||
|
||||
const overflow = users.length - avatarLimit;
|
||||
|
@ -6,16 +6,19 @@ import { RoleDescription } from 'component/common/RoleDescription/RoleDescriptio
|
||||
|
||||
interface IRoleBadgeProps {
|
||||
roleId: number;
|
||||
hideIcon?: boolean;
|
||||
children?: string;
|
||||
}
|
||||
|
||||
export const RoleBadge = ({ roleId, children }: IRoleBadgeProps) => {
|
||||
export const RoleBadge = ({ roleId, hideIcon, children }: IRoleBadgeProps) => {
|
||||
const { role } = useRole(roleId.toString());
|
||||
|
||||
const icon = hideIcon ? undefined : <UserIcon />;
|
||||
|
||||
if (!role) {
|
||||
if (children)
|
||||
return (
|
||||
<Badge color='success' icon={<UserIcon />}>
|
||||
<Badge color='success' icon={icon}>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
@ -24,11 +27,7 @@ export const RoleBadge = ({ roleId, children }: IRoleBadgeProps) => {
|
||||
|
||||
return (
|
||||
<HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />} arrow>
|
||||
<Badge
|
||||
color='success'
|
||||
icon={<UserIcon />}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Badge color='success' icon={icon} sx={{ cursor: 'pointer' }}>
|
||||
{role.name}
|
||||
</Badge>
|
||||
</HtmlTooltip>
|
||||
|
@ -8,6 +8,9 @@ interface IStringTruncatorProps {
|
||||
maxLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Deprecated in favor of Truncator
|
||||
*/
|
||||
const StringTruncator = ({
|
||||
text,
|
||||
maxWidth,
|
||||
|
75
frontend/src/component/common/Truncator/Truncator.tsx
Normal file
75
frontend/src/component/common/Truncator/Truncator.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
type BoxProps,
|
||||
styled,
|
||||
Tooltip,
|
||||
type TooltipProps,
|
||||
} from '@mui/material';
|
||||
|
||||
const StyledTruncatorContainer = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== 'lines',
|
||||
})<{ lines: number }>(({ lines }) => ({
|
||||
lineClamp: `${lines}`,
|
||||
WebkitLineClamp: lines,
|
||||
display: '-webkit-box',
|
||||
boxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
alignItems: 'flex-start',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
wordBreak: 'break-word',
|
||||
}));
|
||||
|
||||
type OverridableTooltipProps = Omit<TooltipProps, 'children'>;
|
||||
|
||||
interface ITruncatorProps extends BoxProps {
|
||||
lines?: number;
|
||||
title?: string;
|
||||
arrow?: boolean;
|
||||
tooltipProps?: OverridableTooltipProps;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Truncator = ({
|
||||
lines = 1,
|
||||
title,
|
||||
arrow,
|
||||
tooltipProps,
|
||||
children,
|
||||
...props
|
||||
}: ITruncatorProps) => {
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const checkTruncation = () => {
|
||||
if (ref.current) {
|
||||
setIsTruncated(ref.current.scrollHeight > ref.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(checkTruncation);
|
||||
if (ref.current) {
|
||||
resizeObserver.observe(ref.current);
|
||||
}
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [title, children]);
|
||||
|
||||
const overridableTooltipProps: OverridableTooltipProps = {
|
||||
title,
|
||||
arrow,
|
||||
...tooltipProps,
|
||||
};
|
||||
|
||||
const { title: tooltipTitle, ...otherTooltipProps } =
|
||||
overridableTooltipProps;
|
||||
|
||||
return (
|
||||
<Tooltip title={isTruncated ? tooltipTitle : ''} {...otherTooltipProps}>
|
||||
<StyledTruncatorContainer ref={ref} lines={lines} {...props}>
|
||||
{children}
|
||||
</StyledTruncatorContainer>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user