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

chore: new release plan template card (#9096)

https://linear.app/unleash/issue/2-3125/improve-release-plan-template-cards

Improves the release plan template cards.

This PR introduces a new reusable `Card` component to help us render
cards with the new design. The GroupCard is also adapted to use this new
`Card` component in this PR, since that was the latest one to be
upgraded, however other items like projects and integrations are not. We
can migrate them to this new component at a later stage in separate PRs.

### Before


![image](https://github.com/user-attachments/assets/623454c7-77e9-4672-ad5b-cb6bd7cbf7f2)

### After


![image](https://github.com/user-attachments/assets/20bff73e-80d2-41b5-8f8b-de1c76e69caf)
This commit is contained in:
Nuno Góis 2025-01-14 13:49:50 +00:00 committed by GitHub
parent b5f0d3e86a
commit 3eeab7e80b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 483 additions and 466 deletions

View File

@ -1,21 +1,15 @@
import { Box, Card, styled } from '@mui/material';
import { styled } from '@mui/material';
import type { IGroup } from 'interfaces/group';
import { Link, useNavigate } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge } from 'component/common/Badge/Badge';
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import { Link } from 'react-router-dom';
import { GroupCardActions } from './GroupCardActions';
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';
import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
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';
import { Card } from 'component/common/Card/Card';
import { GroupCardFooter } from './GroupCardFooter';
const StyledCardLink = styled(Link)(({ theme }) => ({
color: 'inherit',
@ -27,54 +21,6 @@ const StyledCardLink = styled(Link)(({ theme }) => ({
pointer: 'cursor',
}));
const StyledCard = styled(Card)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
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': {
backgroundColor: theme.palette.neutral.light,
},
borderRadius: theme.shape.borderRadiusMedium,
}));
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 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 StyledCardTitle = styled('h3')(({ theme }) => ({
margin: 0,
marginRight: 'auto',
@ -83,47 +29,6 @@ const StyledCardTitle = styled('h3')(({ theme }) => ({
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),
maxWidth: theme.spacing(25),
}));
const StyledProjectBadge = styled(Badge)({
cursor: 'pointer',
overflowWrap: 'anywhere',
});
interface IGroupCardProps {
group: IGroup;
onEditUsers: (group: IGroup) => void;
@ -135,115 +40,48 @@ export const GroupCard = ({
onEditUsers,
onRemoveGroup,
}: IGroupCardProps) => {
const navigate = useNavigate();
const { searchQuery } = useSearchHighlightContext();
const {
settings: { enabled: scimEnabled },
} = useScimSettings();
const isScimGroup = scimEnabled && Boolean(group.scimId);
return (
const title = (
<Truncator title={group.name} arrow component={StyledCardTitle}>
<Highlighter search={searchQuery}>{group.name}</Highlighter>
</Truncator>
);
const headerActions = (
<>
<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}
/>
</StyledCardBodyHeader>
<ConditionallyRender
condition={Boolean(group.description)}
show={
<Truncator
lines={2}
title={group.description}
arrow
component={StyledCardDescription}
>
<Highlighter search={searchQuery}>
{group.description}
</Highlighter>
</Truncator>
}
/>
</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>
{group.rootRole && <RoleBadge roleId={group.rootRole!} hideIcon />}
<GroupCardActions
groupId={group.id}
onEditUsers={() => onEditUsers(group)}
onRemove={() => onRemoveGroup(group)}
isScimGroup={isScimGroup}
/>
</>
);
const body = group.description && (
<Truncator lines={2} title={group.description} arrow>
<Highlighter search={searchQuery}>{group.description}</Highlighter>
</Truncator>
);
return (
<StyledCardLink to={`/admin/groups/${group.id}`}>
<Card
title={title}
icon={<GroupsIcon />}
headerActions={headerActions}
footer={<GroupCardFooter group={group} />}
>
{body}
</Card>
</StyledCardLink>
);
};

View File

@ -1,52 +0,0 @@
import { Popover, styled } from '@mui/material';
import type { IGroupUser } from 'interfaces/group';
const StyledPopover = styled(Popover)(({ theme }) => ({
pointerEvents: 'none',
'.MuiPaper-root': {
padding: theme.spacing(2),
},
}));
const StyledName = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
marginTop: theme.spacing(1),
}));
interface IGroupPopoverProps {
user: Partial<IGroupUser & { description?: string }> | undefined;
open: boolean;
anchorEl: HTMLElement | null;
onPopoverClose(event: React.MouseEvent<HTMLElement>): void;
}
export const GroupPopover = ({
user,
open,
anchorEl,
onPopoverClose,
}: IGroupPopoverProps) => {
return (
<StyledPopover
open={open}
anchorEl={anchorEl}
onClose={onPopoverClose}
disableScrollLock={true}
disableRestoreFocus={true}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<StyledName>{user?.name || user?.username}</StyledName>
<div>{user?.description || user?.email}</div>
</StyledPopover>
);
};

View File

@ -0,0 +1,78 @@
import type { IGroup } from 'interfaces/group';
import { Badge } from 'component/common/Badge/Badge';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import {
AvatarComponent,
AvatarGroup,
} from 'component/common/AvatarGroup/AvatarGroup';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { Box, styled } from '@mui/material';
import { useNavigate } from 'react-router-dom';
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),
maxWidth: theme.spacing(25),
}));
const StyledProjectBadge = styled(Badge)({
cursor: 'pointer',
overflowWrap: 'anywhere',
});
interface IGroupCardFooterProps {
group: IGroup;
}
export const GroupCardFooter = ({ group }: IGroupCardFooterProps) => {
const navigate = useNavigate();
return (
<>
{group.users.length > 0 ? (
<AvatarGroup
users={group.users}
AvatarComponent={StyledAvatarComponent}
/>
) : (
<span>This group has no users</span>
)}
{group.projects.length > 0 && (
<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>
}
>
<span>
{group.projects.length} project
{group.projects.length !== 1 && 's'}
</span>
</TooltipLink>
)}
</>
);
};

View File

@ -0,0 +1,112 @@
import { styled, Card as MUICard, Box } from '@mui/material';
const StyledCard = styled(MUICard)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
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': {
backgroundColor: theme.palette.neutral.light,
},
borderRadius: theme.shape.borderRadiusMedium,
}));
const StyledCardBody = styled(Box)(({ theme }) => ({
padding: theme.spacing(2),
display: 'flex',
flexFlow: 'column',
height: '100%',
position: 'relative',
}));
const StyledCardBodyHeader = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
width: '100%',
alignItems: 'center',
justifyContent: 'space-between',
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.body1.fontSize,
lineHeight: '1.2',
}));
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 StyledCardActions = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
marginLeft: 'auto',
}));
const StyledCardBodyContent = styled(Box)(({ 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),
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
textWrap: 'nowrap',
}));
interface ICardProps {
icon?: React.ReactNode;
title?: React.ReactNode;
headerActions?: React.ReactNode;
footer?: React.ReactNode;
children?: React.ReactNode;
}
export const Card = ({
icon,
title,
headerActions,
footer,
children,
}: ICardProps) => (
<StyledCard>
<StyledCardBody>
<StyledCardBodyHeader>
{icon && (
<StyledCardIconContainer>{icon}</StyledCardIconContainer>
)}
{title}
{headerActions && (
<StyledCardActions>{headerActions}</StyledCardActions>
)}
</StyledCardBodyHeader>
{children && (
<StyledCardBodyContent>{children}</StyledCardBodyContent>
)}
</StyledCardBody>
{footer && <StyledCardFooter>{footer}</StyledCardFooter>}
</StyledCard>
);

View File

@ -1,105 +0,0 @@
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplates.svg';
import { styled, Typography } from '@mui/material';
import { ReleasePlanTemplateCardMenu } from './ReleasePlanTemplateCardMenu';
import { useNavigate } from 'react-router-dom';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';
const StyledTemplateCard = styled('aside')(({ theme }) => ({
height: '100%',
cursor: 'pointer',
'&:hover': {
transition: 'background-color 0.2s ease-in-out',
backgroundColor: theme.palette.neutral.light,
},
overflow: 'hidden',
}));
const TemplateCardHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(2.5),
borderTopLeftRadius: theme.shape.borderRadiusLarge,
borderTopRightRadius: theme.shape.borderRadiusLarge,
}));
const TemplateCardBody = styled('div')(({ theme }) => ({
padding: theme.spacing(1.25),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusLarge,
borderTop: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
display: 'flex',
flexDirection: 'column',
}));
const StyledCenter = styled('div')(({ theme }) => ({
textAlign: 'center',
}));
const StyledDiv = styled('div')(({ theme }) => ({
display: 'flex',
}));
const StyledCreatedBy = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
display: 'flex',
alignItems: 'center',
marginRight: 'auto',
gap: theme.spacing(1),
}));
const StyledCreatedByAvatar = styled(UserAvatar)(({ theme }) => ({
width: theme.spacing(3),
height: theme.spacing(3),
}));
const StyledMenu = styled('div')(({ theme }) => ({
marginLeft: theme.spacing(1),
marginTop: theme.spacing(-1),
marginBottom: theme.spacing(-1),
marginRight: theme.spacing(-1),
display: 'flex',
alignItems: 'center',
}));
export const ReleasePlanTemplateCard = ({
template,
}: { template: IReleasePlanTemplate }) => {
const navigate = useNavigate();
const onClick = () => {
navigate(`/release-management/edit/${template.id}`);
};
const { user: createdBy } = useUserInfo(`${template.createdByUserId}`);
return (
<StyledTemplateCard onClick={onClick}>
<TemplateCardHeader>
<StyledCenter>
<ReleaseTemplateIcon />
</StyledCenter>
</TemplateCardHeader>
<TemplateCardBody>
<div>{template.name}</div>
<StyledDiv>
<StyledCreatedBy>
Created by <StyledCreatedByAvatar user={createdBy} />
</StyledCreatedBy>
<StyledMenu
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<ReleasePlanTemplateCardMenu
template={template}
onClick={onClick}
/>
</StyledMenu>
</StyledDiv>
</TemplateCardBody>
</StyledTemplateCard>
);
};

View File

@ -0,0 +1,55 @@
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplates.svg';
import { styled } from '@mui/material';
import { Link } from 'react-router-dom';
import { Card } from 'component/common/Card/Card';
import { Truncator } from 'component/common/Truncator/Truncator';
import { ReleasePlanTemplateCardActions } from './ReleasePlanTemplateCardActions';
import { ReleasePlanTemplateCardFooter } from './ReleasePlanTemplateCardFooter';
const StyledCardLink = styled(Link)(({ theme }) => ({
color: 'inherit',
textDecoration: 'none',
border: 'none',
padding: '0',
background: 'transparent',
fontFamily: theme.typography.fontFamily,
pointer: 'cursor',
}));
const StyledCardTitle = styled('h3')(({ theme }) => ({
margin: 0,
marginRight: 'auto',
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.body1.fontSize,
lineHeight: '1.2',
}));
export const ReleasePlanTemplateCard = ({
template,
}: { template: IReleasePlanTemplate }) => (
<StyledCardLink to={`/release-management/edit/${template.id}`}>
<Card
icon={<ReleaseTemplateIcon />}
title={
<Truncator
title={template.name}
arrow
component={StyledCardTitle}
>
{template.name}
</Truncator>
}
headerActions={
<ReleasePlanTemplateCardActions template={template} />
}
footer={<ReleasePlanTemplateCardFooter template={template} />}
>
{template.description && (
<Truncator lines={2} title={template.description} arrow>
{template.description}
</Truncator>
)}
</Card>
</StyledCardLink>
);

View File

@ -0,0 +1,138 @@
import { useCallback, useState } from 'react';
import {
IconButton,
Tooltip,
MenuItem,
ListItemText,
styled,
Popover,
MenuList,
ListItemIcon,
Typography,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { useReleasePlanTemplatesApi } from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { TemplateDeleteDialog } from '../TemplateDeleteDialog';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { Link } from 'react-router-dom';
const StyledActions = styled('div')(({ theme }) => ({
margin: theme.spacing(-1),
marginLeft: theme.spacing(-0.5),
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}));
const StyledPopover = styled(Popover)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(1, 1.5),
}));
export const ReleasePlanTemplateCardActions = ({
template,
}: { template: IReleasePlanTemplate }) => {
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const { deleteReleasePlanTemplate } = useReleasePlanTemplatesApi();
const { refetch } = useReleasePlanTemplates();
const { setToastData, setToastApiError } = useToast();
const [deleteOpen, setDeleteOpen] = useState(false);
const deleteReleasePlan = useCallback(async () => {
try {
await deleteReleasePlanTemplate(template.id);
refetch();
setToastData({
type: 'success',
text: 'Release plan template deleted',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}, [setToastApiError, refetch, setToastData, deleteReleasePlanTemplate]);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const id = `release-plan-template-${template.id}-actions`;
const menuId = `${id}-menu`;
return (
<StyledActions
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<Tooltip title='Release plan template actions' arrow describeChild>
<IconButton
id={id}
aria-controls={open ? 'actions-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
type='button'
size='small'
>
<MoreVertIcon />
</IconButton>
</Tooltip>
<StyledPopover
id={menuId}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
disableScrollLock={true}
>
<MenuList aria-labelledby={id}>
<MenuItem
onClick={handleClose}
component={Link}
to={`/release-management/edit/${template.id}`}
>
<ListItemIcon>
<EditIcon />
</ListItemIcon>
<ListItemText>
<Typography variant='body2'>
Edit template
</Typography>
</ListItemText>
</MenuItem>
<MenuItem
onClick={() => {
setDeleteOpen(true);
handleClose();
}}
>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText>
<Typography variant='body2'>
Delete template
</Typography>
</ListItemText>
</MenuItem>
</MenuList>
</StyledPopover>
<TemplateDeleteDialog
template={template}
open={deleteOpen}
setOpen={setDeleteOpen}
onConfirm={deleteReleasePlan}
/>
</StyledActions>
);
};

View File

@ -0,0 +1,60 @@
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { Box, styled } from '@mui/material';
import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';
import theme from 'themes/theme';
import { AvatarComponent } from 'component/common/AvatarGroup/AvatarGroup';
const StyledFooter = styled(Box)({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
});
const StyledAvatar = styled(AvatarComponent)(({ theme }) => ({
height: theme.spacing(3.5),
width: theme.spacing(3.5),
marginLeft: 0,
}));
const StyledContainer = styled(Box)({
display: 'flex',
flexDirection: 'column',
});
const StyledValue = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
lineHeight: 1,
lineClamp: `1`,
WebkitLineClamp: 1,
display: '-webkit-box',
boxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden',
alignItems: 'flex-start',
WebkitBoxOrient: 'vertical',
wordBreak: 'break-word',
maxWidth: '100%',
color: theme.palette.text.primary,
}));
interface IReleasePlanTemplateCardFooterProps {
template: IReleasePlanTemplate;
}
export const ReleasePlanTemplateCardFooter = ({
template,
}: IReleasePlanTemplateCardFooterProps) => {
const { user: createdBy } = useUserInfo(`${template.createdByUserId}`);
return (
<StyledFooter>
<StyledAvatar user={createdBy} />
<StyledContainer>
<span>Created by</span>
<StyledValue>
{createdBy.name || createdBy.username || createdBy.email}
</StyledValue>
</StyledContainer>
</StyledFooter>
);
};

View File

@ -1,107 +0,0 @@
import { useCallback, useState } from 'react';
import {
IconButton,
Tooltip,
Menu,
MenuItem,
ListItemText,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { useReleasePlanTemplatesApi } from 'hooks/api/actions/useReleasePlanTemplatesApi/useReleasePlanTemplatesApi';
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { TemplateDeleteDialog } from './TemplateDeleteDialog';
export const ReleasePlanTemplateCardMenu = ({
template,
onClick,
}: { template: IReleasePlanTemplate; onClick: () => void }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const { deleteReleasePlanTemplate } = useReleasePlanTemplatesApi();
const { refetch } = useReleasePlanTemplates();
const { setToastData, setToastApiError } = useToast();
const [deleteOpen, setDeleteOpen] = useState(false);
const deleteReleasePlan = useCallback(async () => {
try {
await deleteReleasePlanTemplate(template.id);
refetch();
setToastData({
type: 'success',
text: 'Release plan template deleted',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
}, [setToastApiError, refetch, setToastData, deleteReleasePlanTemplate]);
const closeMenu = () => {
setIsMenuOpen(false);
setAnchorEl(null);
};
const handleMenuClick = (event: React.SyntheticEvent) => {
event.stopPropagation();
if (isMenuOpen) {
closeMenu();
} else {
setAnchorEl(event.currentTarget);
setIsMenuOpen(true);
}
};
return (
<>
<Tooltip title='Release plan template actions' arrow describeChild>
<IconButton
id={template.id}
aria-controls={isMenuOpen ? 'actions-menu' : undefined}
aria-haspopup='true'
aria-expanded={isMenuOpen ? 'true' : undefined}
onClick={handleMenuClick}
type='button'
>
<MoreVertIcon />
</IconButton>
</Tooltip>
<Menu
id='project-card-menu'
open={Boolean(anchorEl)}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
onClose={handleMenuClick}
>
<MenuItem
onClick={() => {
onClick();
}}
>
<ListItemText>Edit template</ListItemText>
</MenuItem>
<MenuItem
onClick={() => {
setDeleteOpen(true);
closeMenu();
}}
>
<ListItemText>Delete template</ListItemText>
</MenuItem>
</Menu>
<TemplateDeleteDialog
template={template}
open={deleteOpen}
setOpen={setDeleteOpen}
onConfirm={deleteReleasePlan}
/>
</>
);
};

View File

@ -1,5 +1,5 @@
import { Grid } from '@mui/material';
import { ReleasePlanTemplateCard } from './ReleasePlanTemplateCard';
import { ReleasePlanTemplateCard } from './ReleasePlanTemplateCard/ReleasePlanTemplateCard';
import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
interface ITemplateList {