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  ### After 
This commit is contained in:
parent
b5f0d3e86a
commit
3eeab7e80b
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
112
frontend/src/component/common/Card/Card.tsx
Normal file
112
frontend/src/component/common/Card/Card.tsx
Normal 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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user