mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02: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  ### After  Hovering over the "X projects" label reveals the projects the group belongs to. You can navigate to any project by clicking its badge.  Truncated titles and descriptions show a tooltip with the full text on hover.  
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 type { IGroup } from 'interfaces/group';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
@ -7,88 +7,119 @@ import { GroupCardActions } from './GroupCardActions/GroupCardActions';
|
|||||||
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
||||||
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';
|
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';
|
||||||
import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
|
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',
|
textDecoration: 'none',
|
||||||
color: theme.palette.text.primary,
|
border: 'none',
|
||||||
|
padding: '0',
|
||||||
|
background: 'transparent',
|
||||||
|
fontFamily: theme.typography.fontFamily,
|
||||||
|
pointer: 'cursor',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledGroupCard = styled('aside')(({ theme }) => ({
|
const StyledCard = styled(Card)(({ theme }) => ({
|
||||||
padding: theme.spacing(2.5),
|
|
||||||
height: '100%',
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
|
||||||
boxShadow: theme.boxShadows.card,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
[theme.breakpoints.up('md')]: {
|
justifyContent: 'space-between',
|
||||||
padding: theme.spacing(4),
|
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': {
|
'&:hover': {
|
||||||
transition: 'background-color 0.2s ease-in-out',
|
|
||||||
backgroundColor: theme.palette.neutral.light,
|
backgroundColor: theme.palette.neutral.light,
|
||||||
},
|
},
|
||||||
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledRow = styled('div')(() => ({
|
const StyledCardBody = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCardBodyHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
width: '100%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTitleRow = styled(StyledRow)(() => ({
|
const StyledCardIconContainer = styled(Box)(({ theme }) => ({
|
||||||
alignItems: 'flex-start',
|
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 }) => ({
|
const StyledCardTitle = styled('h3')(({ theme }) => ({
|
||||||
marginTop: 'auto',
|
margin: 0,
|
||||||
alignItems: 'flex-end',
|
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),
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeaderTitle = styled('h2')(({ theme }) => ({
|
const StyledProjectBadge = styled(Badge)({
|
||||||
fontSize: theme.fontSizes.mainHeader,
|
cursor: 'pointer',
|
||||||
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',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IGroupCardProps {
|
interface IGroupCardProps {
|
||||||
@ -104,6 +135,8 @@ export const GroupCard = ({
|
|||||||
}: IGroupCardProps) => {
|
}: IGroupCardProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
settings: { enabled: scimEnabled },
|
settings: { enabled: scimEnabled },
|
||||||
} = useScimSettings();
|
} = useScimSettings();
|
||||||
@ -111,84 +144,104 @@ export const GroupCard = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledLink key={group.id} to={`/admin/groups/${group.id}`}>
|
<StyledCardLink to={`/admin/groups/${group.id}`}>
|
||||||
<StyledGroupCard>
|
<StyledCard>
|
||||||
<StyledTitleRow>
|
<StyledCardBody>
|
||||||
<StyledHeaderTitle>{group.name}</StyledHeaderTitle>
|
<StyledCardBodyHeader>
|
||||||
<StyledHeaderActions>
|
<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
|
<GroupCardActions
|
||||||
groupId={group.id}
|
groupId={group.id}
|
||||||
onEditUsers={() => onEditUsers(group)}
|
onEditUsers={() => onEditUsers(group)}
|
||||||
onRemove={() => onRemoveGroup(group)}
|
onRemove={() => onRemoveGroup(group)}
|
||||||
isScimGroup={isScimGroup}
|
isScimGroup={isScimGroup}
|
||||||
/>
|
/>
|
||||||
</StyledHeaderActions>
|
</StyledCardBodyHeader>
|
||||||
</StyledTitleRow>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(group.rootRole)}
|
|
||||||
show={
|
|
||||||
<InfoBadgeDescription>
|
|
||||||
<p>Root role:</p>
|
|
||||||
<RoleBadge roleId={group.rootRole!} />
|
|
||||||
</InfoBadgeDescription>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StyledDescription>{group.description}</StyledDescription>
|
|
||||||
<StyledBottomRow>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={group.users?.length > 0}
|
condition={Boolean(group.description)}
|
||||||
show={<AvatarGroup users={group.users} />}
|
show={
|
||||||
elseShow={
|
<Truncator
|
||||||
<StyledCounterDescription>
|
lines={2}
|
||||||
This group has no users.
|
title={group.description}
|
||||||
</StyledCounterDescription>
|
arrow
|
||||||
|
component={StyledCardDescription}
|
||||||
|
>
|
||||||
|
<Highlighter search={searchQuery}>
|
||||||
|
{group.description}
|
||||||
|
</Highlighter>
|
||||||
|
</Truncator>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ProjectBadgeContainer>
|
</StyledCardBody>
|
||||||
<ConditionallyRender
|
<StyledCardFooter>
|
||||||
condition={group.projects.length > 0}
|
<ConditionallyRender
|
||||||
show={group.projects.map((project) => (
|
condition={group.users.length > 0}
|
||||||
<Tooltip
|
show={
|
||||||
key={project}
|
<AvatarGroup
|
||||||
title='View project'
|
users={group.users}
|
||||||
arrow
|
AvatarComponent={StyledAvatarComponent}
|
||||||
placement='bottom-end'
|
/>
|
||||||
describeChild
|
}
|
||||||
>
|
elseShow={
|
||||||
<ProjectNameBadge
|
<StyledCardFooterSpan>
|
||||||
onClick={(e) => {
|
This group has no users
|
||||||
e.preventDefault();
|
</StyledCardFooterSpan>
|
||||||
navigate(
|
}
|
||||||
`/projects/${project}/settings/access`,
|
/>
|
||||||
);
|
<ConditionallyRender
|
||||||
}}
|
condition={group.projects.length > 0}
|
||||||
color='secondary'
|
show={
|
||||||
icon={<TopicOutlinedIcon />}
|
<TooltipLink
|
||||||
>
|
component='span'
|
||||||
{project}
|
tooltip={
|
||||||
</ProjectNameBadge>
|
<StyledProjectsTooltip>
|
||||||
</Tooltip>
|
{group.projects.map((project) => (
|
||||||
))}
|
<StyledProjectBadge
|
||||||
elseShow={
|
key={project}
|
||||||
<ConditionallyRender
|
onClick={(e) => {
|
||||||
condition={!group.rootRole}
|
e.preventDefault();
|
||||||
show={
|
navigate(
|
||||||
<Tooltip
|
`/projects/${project}/settings/access`,
|
||||||
title='This group is not used in any project'
|
);
|
||||||
arrow
|
}}
|
||||||
describeChild
|
color='secondary'
|
||||||
>
|
icon={<TopicOutlinedIcon />}
|
||||||
<Badge>Not used</Badge>
|
>
|
||||||
</Tooltip>
|
{project}
|
||||||
}
|
</StyledProjectBadge>
|
||||||
/>
|
))}
|
||||||
}
|
</StyledProjectsTooltip>
|
||||||
/>
|
}
|
||||||
</ProjectBadgeContainer>
|
>
|
||||||
</StyledBottomRow>
|
<StyledCardFooterSpan>
|
||||||
</StyledGroupCard>
|
{group.projects.length} project
|
||||||
</StyledLink>
|
{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';
|
import { scimGroupTooltip } from 'component/admin/groups/group-constants';
|
||||||
|
|
||||||
const StyledActions = styled('div')(({ theme }) => ({
|
const StyledActions = styled('div')(({ theme }) => ({
|
||||||
|
margin: theme.spacing(-1),
|
||||||
|
marginLeft: theme.spacing(-0.5),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
transform: 'translate3d(8px, -6px, 0)',
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledPopover = styled(Popover)(({ theme }) => ({
|
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||||
@ -51,7 +53,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = `feature-${groupId}-actions`;
|
const id = `group-${groupId}-actions`;
|
||||||
const menuId = `${id}-menu`;
|
const menuId = `${id}-menu`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,6 +76,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
|||||||
aria-expanded={open ? 'true' : undefined}
|
aria-expanded={open ? 'true' : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type='button'
|
type='button'
|
||||||
|
size='small'
|
||||||
>
|
>
|
||||||
<MoreVert />
|
<MoreVert />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -6,7 +6,7 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
|||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Search } from 'component/common/Search/Search';
|
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 theme from 'themes/theme';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { TablePlaceholder } from 'component/common/Table';
|
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 { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
|
||||||
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
|
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>>;
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
const groupsSearch = (group: IGroup, searchValue: string) => {
|
const groupsSearch = (group: IGroup, searchValue: string) => {
|
||||||
@ -131,17 +137,16 @@ export const GroupsList: VFC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SearchHighlightProvider value={searchValue}>
|
<SearchHighlightProvider value={searchValue}>
|
||||||
<Grid container spacing={2}>
|
<StyledGridContainer>
|
||||||
{data.map((group) => (
|
{data.map((group) => (
|
||||||
<Grid key={group.id} item xs={12} md={6}>
|
<GroupCard
|
||||||
<GroupCard
|
key={group.id}
|
||||||
group={group}
|
group={group}
|
||||||
onEditUsers={onEditUsers}
|
onEditUsers={onEditUsers}
|
||||||
onRemoveGroup={onRemoveGroup}
|
onRemoveGroup={onRemoveGroup}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</StyledGridContainer>
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!loading && data.length === 0}
|
condition={!loading && data.length === 0}
|
||||||
|
@ -85,7 +85,7 @@ const AvatarGroupInner = ({
|
|||||||
return 0;
|
return 0;
|
||||||
})
|
})
|
||||||
.slice(0, avatarLimit),
|
.slice(0, avatarLimit),
|
||||||
[users],
|
[users, avatarLimit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const overflow = users.length - avatarLimit;
|
const overflow = users.length - avatarLimit;
|
||||||
|
@ -6,16 +6,19 @@ import { RoleDescription } from 'component/common/RoleDescription/RoleDescriptio
|
|||||||
|
|
||||||
interface IRoleBadgeProps {
|
interface IRoleBadgeProps {
|
||||||
roleId: number;
|
roleId: number;
|
||||||
|
hideIcon?: boolean;
|
||||||
children?: string;
|
children?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RoleBadge = ({ roleId, children }: IRoleBadgeProps) => {
|
export const RoleBadge = ({ roleId, hideIcon, children }: IRoleBadgeProps) => {
|
||||||
const { role } = useRole(roleId.toString());
|
const { role } = useRole(roleId.toString());
|
||||||
|
|
||||||
|
const icon = hideIcon ? undefined : <UserIcon />;
|
||||||
|
|
||||||
if (!role) {
|
if (!role) {
|
||||||
if (children)
|
if (children)
|
||||||
return (
|
return (
|
||||||
<Badge color='success' icon={<UserIcon />}>
|
<Badge color='success' icon={icon}>
|
||||||
{children}
|
{children}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@ -24,11 +27,7 @@ export const RoleBadge = ({ roleId, children }: IRoleBadgeProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />} arrow>
|
<HtmlTooltip title={<RoleDescription roleId={roleId} tooltip />} arrow>
|
||||||
<Badge
|
<Badge color='success' icon={icon} sx={{ cursor: 'pointer' }}>
|
||||||
color='success'
|
|
||||||
icon={<UserIcon />}
|
|
||||||
sx={{ cursor: 'pointer' }}
|
|
||||||
>
|
|
||||||
{role.name}
|
{role.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HtmlTooltip>
|
</HtmlTooltip>
|
||||||
|
@ -8,6 +8,9 @@ interface IStringTruncatorProps {
|
|||||||
maxLength: number;
|
maxLength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Deprecated in favor of Truncator
|
||||||
|
*/
|
||||||
const StringTruncator = ({
|
const StringTruncator = ({
|
||||||
text,
|
text,
|
||||||
maxWidth,
|
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