mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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