mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
feat: Project owners UI (#6949)
--------- Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
3978c690e0
commit
b6865a5a9d
BIN
frontend/public/logo-unleash.png
Normal file
BIN
frontend/public/logo-unleash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
5
frontend/src/assets/icons/projectIconSmall.svg
Normal file
5
frontend/src/assets/icons/projectIconSmall.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="35" viewBox="0 0 37 35" fill="none">
|
||||||
|
<rect x="8.9864" y="9.93245" width="3.78378" height="13.2432" rx="1.41892" fill="#6C65E5" stroke="#6C65E5" stroke-width="0.945947" />
|
||||||
|
<rect x="16.5542" y="13.7161" width="3.78378" height="9.45946" rx="1.41892" fill="#6C65E5" stroke="#6C65E5" stroke-width="0.945947" />
|
||||||
|
<rect x="24.1217" y="18.4456" width="3.78378" height="4.72973" rx="1.41892" fill="#6C65E5" stroke="#6C65E5" stroke-width="0.945947" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 519 B |
@ -25,6 +25,9 @@ interface IGroupCardAvatarsProps {
|
|||||||
users: IGroupUser[];
|
users: IGroupUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Remove after with `projectsListNewCards` flag
|
||||||
|
*/
|
||||||
export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
|
||||||
const shownUsers = useMemo(
|
const shownUsers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -15,7 +15,7 @@ const StyledName = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IGroupPopoverProps {
|
interface IGroupPopoverProps {
|
||||||
user: IGroupUser | undefined;
|
user: Partial<IGroupUser & { description?: string }> | undefined;
|
||||||
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
anchorEl: HTMLElement | null;
|
anchorEl: HTMLElement | null;
|
||||||
@ -44,7 +44,7 @@ export const GroupPopover = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledName>{user?.name || user?.username}</StyledName>
|
<StyledName>{user?.name || user?.username}</StyledName>
|
||||||
<div>{user?.email}</div>
|
<div>{user?.description || user?.email}</div>
|
||||||
</StyledPopover>
|
</StyledPopover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,128 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import type { IGroupUser } from 'interfaces/group';
|
||||||
|
import type React from 'react';
|
||||||
|
import { type ReactNode, useMemo, useState } from 'react';
|
||||||
|
import { GroupPopover } from './GroupPopover/GroupPopover';
|
||||||
|
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
|
||||||
|
import { objectId } from 'utils/objectId';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAvatars = styled('div')(({ theme }) => ({
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
|
||||||
|
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
|
||||||
|
marginLeft: theme.spacing(-1),
|
||||||
|
'&:hover': {
|
||||||
|
outlineColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledUsername = styled('div')(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||||
|
margin: theme.spacing(0, 0, 1),
|
||||||
|
fontSize: theme.typography.caption.fontSize,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IGroupCardAvatarsProps {
|
||||||
|
users: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}[];
|
||||||
|
header?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GroupCardAvatars = ({
|
||||||
|
users = [],
|
||||||
|
header = null,
|
||||||
|
}: IGroupCardAvatarsProps) => {
|
||||||
|
const shownUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
users
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (
|
||||||
|
Object.hasOwn(a, 'joinedAt') &&
|
||||||
|
Object.hasOwn(b, 'joinedAt')
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
(b as IGroupUser)?.joinedAt!.getTime() -
|
||||||
|
(a as IGroupUser)?.joinedAt!.getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.slice(0, 9),
|
||||||
|
[users],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
const [popupUser, setPopupUser] = useState<{
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onPopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPopoverClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarOpen = Boolean(anchorEl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={typeof header === 'string'}
|
||||||
|
show={<StyledHeader>{header}</StyledHeader>}
|
||||||
|
elseShow={header}
|
||||||
|
/>
|
||||||
|
<StyledAvatars>
|
||||||
|
{shownUsers.map((user) => (
|
||||||
|
<StyledAvatar
|
||||||
|
key={objectId(user)}
|
||||||
|
user={{ ...user, id: objectId(user) }}
|
||||||
|
onMouseEnter={(event) => {
|
||||||
|
onPopoverOpen(event);
|
||||||
|
setPopupUser(user);
|
||||||
|
}}
|
||||||
|
onMouseLeave={onPopoverClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={users.length > 9}
|
||||||
|
show={
|
||||||
|
<StyledAvatar>
|
||||||
|
+{users.length - shownUsers.length}
|
||||||
|
</StyledAvatar>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<GroupPopover
|
||||||
|
open={avatarOpen}
|
||||||
|
user={popupUser}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onPopoverClose={onPopoverClose}
|
||||||
|
/>
|
||||||
|
</StyledAvatars>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -59,21 +59,30 @@ const StyledBadge = styled('span')<IBadgeProps>(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const StyledBadgeIcon = styled('span')<IBadgeIconProps>(
|
const StyledBadgeIcon = styled('span')<
|
||||||
({ theme, color = 'neutral', iconRight = false }) => ({
|
IBadgeIconProps & { hasChildren?: boolean }
|
||||||
display: 'flex',
|
>(({ theme, color = 'neutral', iconRight = false, hasChildren }) => ({
|
||||||
color:
|
display: 'flex',
|
||||||
color === 'disabled'
|
color:
|
||||||
? theme.palette.action.disabled
|
color === 'disabled'
|
||||||
: theme.palette[color].main,
|
? theme.palette.action.disabled
|
||||||
margin: iconRight
|
: theme.palette[color].main,
|
||||||
? theme.spacing(0, 0, 0, 0.5)
|
margin: iconRight
|
||||||
: theme.spacing(0, 0.5, 0, 0),
|
? theme.spacing(0, 0, 0, hasChildren ? 0.5 : 0)
|
||||||
}),
|
: theme.spacing(0, hasChildren ? 0.5 : 0, 0, 0),
|
||||||
);
|
}));
|
||||||
|
|
||||||
const BadgeIcon = (color: Color, icon: ReactElement, iconRight = false) => (
|
const BadgeIcon = (
|
||||||
<StyledBadgeIcon color={color} iconRight={iconRight}>
|
color: Color,
|
||||||
|
icon: ReactElement,
|
||||||
|
iconRight = false,
|
||||||
|
hasChildren = false,
|
||||||
|
) => (
|
||||||
|
<StyledBadgeIcon
|
||||||
|
color={color}
|
||||||
|
iconRight={iconRight}
|
||||||
|
hasChildren={hasChildren}
|
||||||
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(icon?.props.sx)}
|
condition={Boolean(icon?.props.sx)}
|
||||||
show={icon}
|
show={icon}
|
||||||
@ -112,12 +121,12 @@ export const Badge: FC<IBadgeProps> = forwardRef(
|
|||||||
>
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(icon) && !iconRight}
|
condition={Boolean(icon) && !iconRight}
|
||||||
show={BadgeIcon(color, icon!)}
|
show={BadgeIcon(color, icon!, false, Boolean(children))}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(icon) && Boolean(iconRight)}
|
condition={Boolean(icon) && Boolean(iconRight)}
|
||||||
show={BadgeIcon(color, icon!, true)}
|
show={BadgeIcon(color, icon!, true, Boolean(children))}
|
||||||
/>
|
/>
|
||||||
</StyledBadge>
|
</StyledBadge>
|
||||||
),
|
),
|
||||||
|
@ -20,7 +20,9 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IUserAvatarProps extends AvatarProps {
|
interface IUserAvatarProps extends AvatarProps {
|
||||||
user?: IUser;
|
user?: Partial<
|
||||||
|
Pick<IUser, 'id' | 'name' | 'email' | 'username' | 'imageUrl'>
|
||||||
|
>;
|
||||||
src?: string;
|
src?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
onMouseEnter?: (event: any) => void;
|
onMouseEnter?: (event: any) => void;
|
||||||
|
@ -57,7 +57,7 @@ const StyledAccordionDetails = styled(AccordionDetails, {
|
|||||||
background: theme.palette.envAccordion.expanded,
|
background: theme.palette.envAccordion.expanded,
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||||
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
boxShadow: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)', // replace this with variable
|
boxShadow: theme.boxShadows.accordionFooter,
|
||||||
|
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
padding: theme.spacing(2, 1),
|
padding: theme.spacing(2, 1),
|
||||||
|
@ -3,6 +3,7 @@ import { Card, Box } from '@mui/material';
|
|||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
import { flexRow } from 'themes/themeStyles';
|
||||||
|
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
|
||||||
|
|
||||||
export const StyledProjectCard = styled(Card)(({ theme }) => ({
|
export const StyledProjectCard = styled(Card)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -31,9 +32,9 @@ export const StyledDivHeader = styled('div')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledH2Title = styled('h2')(({ theme }) => ({
|
export const StyledCardTitle = styled('h3')(({ theme }) => ({
|
||||||
fontWeight: 'normal',
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
fontSize: theme.fontSizes.bodySize,
|
fontSize: theme.typography.body1.fontSize,
|
||||||
lineClamp: '2',
|
lineClamp: '2',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
lineHeight: '1.2',
|
lineHeight: '1.2',
|
||||||
@ -65,13 +66,22 @@ export const StyledDivInfo = styled('div')(({ theme }) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
}));
|
padding: theme.spacing(0, 1),
|
||||||
|
|
||||||
export const StyledDivInfoContainer = styled('div')(() => ({
|
|
||||||
textAlign: 'center',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledParagraphInfo = styled('p')(({ theme }) => ({
|
export const StyledParagraphInfo = styled('p')(({ theme }) => ({
|
||||||
color: theme.palette.primary.dark,
|
color: theme.palette.primary.dark,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
fontSize: theme.typography.body1.fontSize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledIconBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(0.5, 0.5, 0.5, 0),
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
import type React from 'react';
|
|
||||||
import { Menu, MenuItem } from '@mui/material';
|
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
||||||
import { type SyntheticEvent, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { getProjectEditPath } from 'utils/routePathHelpers';
|
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
|
||||||
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { DeleteProjectDialogue } from '../Project/DeleteProject/DeleteProjectDialogue';
|
|
||||||
import {
|
import {
|
||||||
StyledProjectCard,
|
StyledProjectCard,
|
||||||
StyledDivHeader,
|
StyledDivHeader,
|
||||||
StyledBox,
|
StyledBox,
|
||||||
StyledH2Title,
|
StyledCardTitle,
|
||||||
StyledEditIcon,
|
|
||||||
StyledDivInfo,
|
StyledDivInfo,
|
||||||
StyledDivInfoContainer,
|
|
||||||
StyledParagraphInfo,
|
StyledParagraphInfo,
|
||||||
StyledProjectCardBody,
|
StyledProjectCardBody,
|
||||||
|
StyledIconBox,
|
||||||
|
StyledProjectIcon,
|
||||||
} from './NewProjectCard.styles';
|
} from './NewProjectCard.styles';
|
||||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
||||||
import { ProjectCardIcon } from './ProjectCardIcon/ProjectCardIcon';
|
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
|
||||||
|
import { ProjectOwners } from './ProjectOwners/ProjectOwners';
|
||||||
|
import type { ProjectSchemaOwners } from 'openapi';
|
||||||
|
|
||||||
interface IProjectCardProps {
|
interface IProjectCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -33,6 +25,7 @@ interface IProjectCardProps {
|
|||||||
onHover: () => void;
|
onHover: () => void;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
mode: string;
|
mode: string;
|
||||||
|
owners?: ProjectSchemaOwners;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectCard = ({
|
export const ProjectCard = ({
|
||||||
@ -44,100 +37,49 @@ export const ProjectCard = ({
|
|||||||
id,
|
id,
|
||||||
mode,
|
mode,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
}: IProjectCardProps) => {
|
owners,
|
||||||
const { isOss } = useUiConfig();
|
}: IProjectCardProps) => (
|
||||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
<StyledProjectCard onMouseEnter={onHover}>
|
||||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
<StyledProjectCardBody>
|
||||||
const navigate = useNavigate();
|
<StyledDivHeader>
|
||||||
|
<StyledIconBox>
|
||||||
const handleClick = (event: React.SyntheticEvent) => {
|
<StyledProjectIcon />
|
||||||
event.preventDefault();
|
</StyledIconBox>
|
||||||
setAnchorEl(event.currentTarget);
|
<StyledBox data-loading>
|
||||||
};
|
<StyledCardTitle>{name}</StyledCardTitle>
|
||||||
|
</StyledBox>
|
||||||
return (
|
<ProjectModeBadge mode={mode} />
|
||||||
<StyledProjectCard onMouseEnter={onHover}>
|
</StyledDivHeader>
|
||||||
<StyledProjectCardBody>
|
<StyledDivInfo>
|
||||||
<StyledDivHeader data-loading>
|
<div>
|
||||||
<ProjectCardIcon mode={mode} />
|
<StyledParagraphInfo data-loading>
|
||||||
<StyledBox>
|
{featureCount}
|
||||||
<StyledH2Title>{name}</StyledH2Title>
|
</StyledParagraphInfo>
|
||||||
</StyledBox>
|
<p data-loading>{featureCount === 1 ? 'flag' : 'flags'}</p>
|
||||||
<PermissionIconButton
|
</div>
|
||||||
style={{ transform: 'translateX(7px)' }}
|
<ConditionallyRender
|
||||||
permission={UPDATE_PROJECT}
|
condition={id !== DEFAULT_PROJECT_ID}
|
||||||
hidden={isOss()}
|
show={
|
||||||
projectId={id}
|
<div>
|
||||||
data-loading
|
<StyledParagraphInfo data-loading>
|
||||||
onClick={handleClick}
|
{memberCount}
|
||||||
tooltipProps={{
|
</StyledParagraphInfo>
|
||||||
title: 'Options',
|
<p data-loading>
|
||||||
}}
|
{memberCount === 1 ? 'member' : 'members'}
|
||||||
>
|
</p>
|
||||||
<MoreVertIcon />
|
</div>
|
||||||
</PermissionIconButton>
|
}
|
||||||
|
/>
|
||||||
<Menu
|
<div>
|
||||||
id='project-card-menu'
|
<StyledParagraphInfo data-loading>
|
||||||
open={Boolean(anchorEl)}
|
{health}%
|
||||||
anchorEl={anchorEl}
|
</StyledParagraphInfo>
|
||||||
style={{ top: 0, left: -100 }}
|
<p data-loading>healthy</p>
|
||||||
onClick={(event) => {
|
</div>
|
||||||
event.preventDefault();
|
</StyledDivInfo>
|
||||||
}}
|
</StyledProjectCardBody>
|
||||||
onClose={(event: SyntheticEvent) => {
|
<ProjectCardFooter id={id} isFavorite={isFavorite}>
|
||||||
event.preventDefault();
|
<ProjectOwners owners={owners} />
|
||||||
setAnchorEl(null);
|
</ProjectCardFooter>
|
||||||
}}
|
</StyledProjectCard>
|
||||||
>
|
);
|
||||||
<MenuItem
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
navigate(getProjectEditPath(id));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledEditIcon />
|
|
||||||
Edit project
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</StyledDivHeader>
|
|
||||||
<StyledDivInfo>
|
|
||||||
<StyledDivInfoContainer>
|
|
||||||
<StyledParagraphInfo data-loading>
|
|
||||||
{featureCount}
|
|
||||||
</StyledParagraphInfo>
|
|
||||||
<p data-loading>toggles</p>
|
|
||||||
</StyledDivInfoContainer>
|
|
||||||
<StyledDivInfoContainer>
|
|
||||||
<StyledParagraphInfo data-loading>
|
|
||||||
{health}%
|
|
||||||
</StyledParagraphInfo>
|
|
||||||
<p data-loading>health</p>
|
|
||||||
</StyledDivInfoContainer>
|
|
||||||
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={id !== DEFAULT_PROJECT_ID}
|
|
||||||
show={
|
|
||||||
<StyledDivInfoContainer>
|
|
||||||
<StyledParagraphInfo data-loading>
|
|
||||||
{memberCount}
|
|
||||||
</StyledParagraphInfo>
|
|
||||||
<p data-loading>members</p>
|
|
||||||
</StyledDivInfoContainer>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledDivInfo>
|
|
||||||
</StyledProjectCardBody>
|
|
||||||
<ProjectCardFooter id={id} isFavorite={isFavorite} />
|
|
||||||
<DeleteProjectDialogue
|
|
||||||
project={id}
|
|
||||||
open={showDelDialog}
|
|
||||||
onClose={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setAnchorEl(null);
|
|
||||||
setShowDelDialog(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledProjectCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { VFC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
@ -14,10 +14,9 @@ const StyledFooter = styled(Box)(({ theme }) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: theme.spacing(1, 2),
|
padding: theme.spacing(1.5, 3),
|
||||||
borderTop: `1px solid ${theme.palette.grey[300]}`,
|
background: theme.palette.envAccordion.expanded,
|
||||||
backgroundColor: theme.palette.grey[100],
|
boxShadow: theme.boxShadows.accordionFooter,
|
||||||
boxShadow: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)', // FIXME: replace with variable
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledFavoriteIconButton = styled(FavoriteIconButton)(({ theme }) => ({
|
const StyledFavoriteIconButton = styled(FavoriteIconButton)(({ theme }) => ({
|
||||||
@ -25,7 +24,8 @@ const StyledFavoriteIconButton = styled(FavoriteIconButton)(({ theme }) => ({
|
|||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProjectCardFooter: VFC<IProjectCardFooterProps> = ({
|
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
||||||
|
children,
|
||||||
id,
|
id,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
}) => {
|
}) => {
|
||||||
@ -48,6 +48,7 @@ export const ProjectCardFooter: VFC<IProjectCardFooterProps> = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<StyledFooter>
|
<StyledFooter>
|
||||||
|
{children}
|
||||||
<StyledFavoriteIconButton
|
<StyledFavoriteIconButton
|
||||||
onClick={onFavorite}
|
onClick={onFavorite}
|
||||||
isFavorite={isFavorite}
|
isFavorite={isFavorite}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import type { VFC } from 'react';
|
|
||||||
import { styled } from '@mui/material';
|
|
||||||
import { Box } from '@mui/material';
|
|
||||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
|
||||||
|
|
||||||
interface IProjectCardIconProps {
|
|
||||||
mode: 'private' | 'protected' | 'public' | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledVisibilityIcon = styled(VisibilityOffIcon)(({ theme }) => ({
|
|
||||||
color: theme.palette.action.disabled,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledLockIcon = styled(LockIcon)(({ theme }) => ({
|
|
||||||
color: theme.palette.action.disabled,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledProjectIcon = styled(BarChartIcon)(({ theme }) => ({
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledIconBox = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: '1px',
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: theme.palette.neutral.border,
|
|
||||||
padding: theme.spacing(0.5),
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ProjectCardIcon: VFC<IProjectCardIconProps> = ({ mode }) => {
|
|
||||||
if (mode === 'private') {
|
|
||||||
return (
|
|
||||||
<StyledIconBox data-loading>
|
|
||||||
<HtmlTooltip
|
|
||||||
title="This project's collaboration mode is set to private. The project and associated feature flags can only be seen by members of the project."
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<StyledVisibilityIcon />
|
|
||||||
</HtmlTooltip>
|
|
||||||
</StyledIconBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'protected') {
|
|
||||||
return (
|
|
||||||
<StyledIconBox data-loading>
|
|
||||||
<HtmlTooltip
|
|
||||||
title="This project's collaboration mode is set to protected. Only admins and project members can submit change requests."
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<StyledLockIcon />
|
|
||||||
</HtmlTooltip>
|
|
||||||
</StyledIconBox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledIconBox data-loading>
|
|
||||||
<StyledProjectIcon />
|
|
||||||
</StyledIconBox>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,35 @@
|
|||||||
|
import type { VFC } from 'react';
|
||||||
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
|
||||||
|
interface IProjectModeBadgeProps {
|
||||||
|
mode: 'private' | 'protected' | 'public' | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectModeBadge: VFC<IProjectModeBadgeProps> = ({ mode }) => {
|
||||||
|
if (mode === 'private') {
|
||||||
|
return (
|
||||||
|
<HtmlTooltip
|
||||||
|
title="This project's collaboration mode is set to private. The project and associated feature flags can only be seen by members of the project."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Badge color='warning' icon={<VisibilityOffIcon />} />
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'protected') {
|
||||||
|
return (
|
||||||
|
<HtmlTooltip
|
||||||
|
title="This project's collaboration mode is set to protected. Only admins and project members can submit change requests."
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Badge color='warning' icon={<LockIcon />} />
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -0,0 +1,75 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { GroupCardAvatars } from 'component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars';
|
||||||
|
import type { ProjectSchema, ProjectSchemaOwners } from 'openapi';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
interface IProjectOwnersProps {
|
||||||
|
owners?: ProjectSchema['owners'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const useOwnersMap = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
owner: ProjectSchemaOwners[0],
|
||||||
|
): {
|
||||||
|
name: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
email?: string;
|
||||||
|
description?: string;
|
||||||
|
} => {
|
||||||
|
if (owner.ownerType === 'user') {
|
||||||
|
return {
|
||||||
|
name: owner.name,
|
||||||
|
imageUrl: owner.imageUrl || undefined,
|
||||||
|
email: owner.email || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (owner.ownerType === 'group') {
|
||||||
|
return {
|
||||||
|
name: owner.name || '',
|
||||||
|
description: 'group',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
description: 'System',
|
||||||
|
imageUrl: `${uiConfig.unleashUrl}/logo-unleash.png`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledUserName = styled('p')(({ theme }) => ({
|
||||||
|
fontSize: theme.typography.body1.fontSize,
|
||||||
|
margin: theme.spacing(0, 0, 0.5, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
|
||||||
|
const ownersMap = useOwnersMap();
|
||||||
|
const users = owners.map(ownersMap);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<GroupCardAvatars
|
||||||
|
header={owners.length === 1 ? 'Owner' : 'Owners'}
|
||||||
|
users={users}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={owners.length === 1}
|
||||||
|
show={
|
||||||
|
<StyledUserName>
|
||||||
|
{users[0]?.name || users[0]?.description}
|
||||||
|
</StyledUserName>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -46,7 +46,7 @@ const StyledAccordionDetails = styled(AccordionDetails, {
|
|||||||
background: theme.palette.envAccordion.expanded,
|
background: theme.palette.envAccordion.expanded,
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||||
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
boxShadow: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)', // replace this with variable
|
boxShadow: theme.boxShadows.accordionFooter,
|
||||||
|
|
||||||
[theme.breakpoints.down('md')]: {
|
[theme.breakpoints.down('md')]: {
|
||||||
padding: theme.spacing(2, 1),
|
padding: theme.spacing(2, 1),
|
||||||
|
@ -8,16 +8,6 @@ import { TablePlaceholder } from 'component/common/Table';
|
|||||||
import { styled, Typography } from '@mui/material';
|
import { styled, Typography } from '@mui/material';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const StyledProjectGroupContainer = styled('article')(({ theme }) => ({
|
|
||||||
h3: {
|
|
||||||
marginBlockEnd: theme.spacing(2),
|
|
||||||
},
|
|
||||||
|
|
||||||
'&+&': {
|
|
||||||
marginBlockStart: theme.spacing(4),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Remove after with `projectsListNewCards` flag
|
* @deprecated Remove after with `projectsListNewCards` flag
|
||||||
*/
|
*/
|
||||||
@ -31,7 +21,7 @@ const StyledDivContainer = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
const StyledGridContainer = styled('div')(({ theme }) => ({
|
const StyledGridContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -59,10 +49,18 @@ export const ProjectGroup: React.FC<{
|
|||||||
: [StyledDivContainer, LegacyProjectCard];
|
: [StyledDivContainer, LegacyProjectCard];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledProjectGroupContainer>
|
<article>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(sectionTitle)}
|
condition={Boolean(sectionTitle)}
|
||||||
show={<Typography component='h3'>{sectionTitle}</Typography>}
|
show={
|
||||||
|
<Typography
|
||||||
|
component='h2'
|
||||||
|
variant='h3'
|
||||||
|
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
|
||||||
|
>
|
||||||
|
{sectionTitle}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={projects.length < 1 && !loading}
|
condition={projects.length < 1 && !loading}
|
||||||
@ -124,6 +122,7 @@ export const ProjectGroup: React.FC<{
|
|||||||
project.featureCount
|
project.featureCount
|
||||||
}
|
}
|
||||||
isFavorite={project.favorite}
|
isFavorite={project.favorite}
|
||||||
|
owners={project.owners}
|
||||||
/>
|
/>
|
||||||
</StyledCardLink>
|
</StyledCardLink>
|
||||||
))}
|
))}
|
||||||
@ -133,6 +132,6 @@ export const ProjectGroup: React.FC<{
|
|||||||
</StyledItemsContainer>
|
</StyledItemsContainer>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledProjectGroupContainer>
|
</article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -28,10 +28,16 @@ import { groupProjects } from './group-projects';
|
|||||||
import { ProjectGroup } from './ProjectGroup';
|
import { ProjectGroup } from './ProjectGroup';
|
||||||
|
|
||||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||||
maxWidth: '400px',
|
maxWidth: '500px',
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(4),
|
||||||
|
}));
|
||||||
|
|
||||||
type PageQueryType = Partial<Record<'search', string>>;
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
type projectMap = {
|
type projectMap = {
|
||||||
@ -148,12 +154,6 @@ export const ProjectListNew = () => {
|
|||||||
hasAccess(CREATE_PROJECT),
|
hasAccess(CREATE_PROJECT),
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderError = () => {
|
|
||||||
return (
|
|
||||||
<StyledApiError onClick={refetch} text='Error fetching projects' />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectCount =
|
const projectCount =
|
||||||
filteredProjects.length < projects.length
|
filteredProjects.length < projects.length
|
||||||
? `${filteredProjects.length} of ${projects.length}`
|
? `${filteredProjects.length} of ${projects.length}`
|
||||||
@ -172,6 +172,7 @@ export const ProjectListNew = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
@ -219,24 +220,36 @@ export const ProjectListNew = () => {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ConditionallyRender condition={error} show={renderError()} />
|
<StyledContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={splitProjectList}
|
condition={error}
|
||||||
show={
|
show={() => (
|
||||||
<>
|
<StyledApiError
|
||||||
<ProjectGroupComponent
|
onClick={refetch}
|
||||||
sectionTitle='My projects'
|
text='Error fetching projects'
|
||||||
projects={groupedProjects.myProjects}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={splitProjectList}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<ProjectGroupComponent
|
||||||
|
sectionTitle='My projects'
|
||||||
|
projects={groupedProjects.myProjects}
|
||||||
|
/>
|
||||||
|
|
||||||
<ProjectGroupComponent
|
<ProjectGroupComponent
|
||||||
sectionTitle='Other projects'
|
sectionTitle='Other projects'
|
||||||
projects={groupedProjects.otherProjects}
|
projects={groupedProjects.otherProjects}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
elseShow={<ProjectGroupComponent projects={filteredProjects} />}
|
elseShow={
|
||||||
/>
|
<ProjectGroupComponent projects={filteredProjects} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ProjectStatsSchema } from 'openapi';
|
import type { ProjectSchema, ProjectStatsSchema } from 'openapi';
|
||||||
import type { IFeatureToggleListItem } from './featureToggle';
|
import type { IFeatureToggleListItem } from './featureToggle';
|
||||||
import type { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
import type { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
||||||
import type { ProjectMode } from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm';
|
import type { ProjectMode } from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm';
|
||||||
@ -13,6 +13,7 @@ export interface IProjectCard {
|
|||||||
mode: string;
|
mode: string;
|
||||||
memberCount?: number;
|
memberCount?: number;
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
|
owners?: ProjectSchema['owners'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeatureNamingType = {
|
export type FeatureNamingType = {
|
||||||
|
@ -28,6 +28,7 @@ const theme = {
|
|||||||
popup: '0px 2px 6px rgba(0, 0, 0, 0.25)',
|
popup: '0px 2px 6px rgba(0, 0, 0, 0.25)',
|
||||||
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
||||||
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
||||||
|
accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)',
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: 'Sen, Roboto, sans-serif',
|
fontFamily: 'Sen, Roboto, sans-serif',
|
||||||
|
@ -20,6 +20,7 @@ export const theme = {
|
|||||||
popup: '0px 2px 6px rgba(0, 0, 0, 0.25)',
|
popup: '0px 2px 6px rgba(0, 0, 0, 0.25)',
|
||||||
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
||||||
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
||||||
|
accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)',
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: 'Sen, Roboto, sans-serif',
|
fontFamily: 'Sen, Roboto, sans-serif',
|
||||||
|
@ -34,6 +34,7 @@ declare module '@mui/material/styles' {
|
|||||||
popup: string;
|
popup: string;
|
||||||
primaryHeader: string;
|
primaryHeader: string;
|
||||||
separator: string;
|
separator: string;
|
||||||
|
accordionFooter: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,16 +207,14 @@ export default class ProjectController extends Controller {
|
|||||||
projectsSchema.$id,
|
projectsSchema.$id,
|
||||||
{ version: 1, projects: serializeDates(projectsWithOwners) },
|
{ version: 1, projects: serializeDates(projectsWithOwners) },
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
return;
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
projectsSchema.$id,
|
||||||
|
{ version: 1, projects: serializeDates(projects) },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
|
||||||
200,
|
|
||||||
res,
|
|
||||||
projectsSchema.$id,
|
|
||||||
{ version: 1, projects: serializeDates(projects) },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeprecatedProjectOverview(
|
async getDeprecatedProjectOverview(
|
||||||
|
@ -4,6 +4,11 @@ import { type IUser, RoleName, type IGroup } from '../../types';
|
|||||||
import { randomId } from '../../util';
|
import { randomId } from '../../util';
|
||||||
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
import { ProjectOwnersReadModel } from './project-owners-read-model';
|
||||||
|
|
||||||
|
jest.mock('../../util', () => ({
|
||||||
|
...jest.requireActual('../../util'),
|
||||||
|
generateImageUrl: jest.fn((input) => `https://${input.image_url}`),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockProjectWithCounts = (name: string) => ({
|
const mockProjectWithCounts = (name: string) => ({
|
||||||
name,
|
name,
|
||||||
id: name,
|
id: name,
|
||||||
@ -170,7 +175,7 @@ describe('integration tests', () => {
|
|||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
name: 'Owner Name',
|
name: 'Owner Name',
|
||||||
email: 'owner@email.com',
|
email: 'owner@email.com',
|
||||||
imageUrl: 'image-url-1',
|
imageUrl: 'https://image-url-1',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -248,7 +253,7 @@ describe('integration tests', () => {
|
|||||||
[projectId]: [
|
[projectId]: [
|
||||||
{
|
{
|
||||||
email: 'owner@email.com',
|
email: 'owner@email.com',
|
||||||
imageUrl: 'image-url-1',
|
imageUrl: 'https://image-url-1',
|
||||||
name: 'Owner Name',
|
name: 'Owner Name',
|
||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
},
|
},
|
||||||
@ -299,13 +304,13 @@ describe('integration tests', () => {
|
|||||||
[projectId]: [
|
[projectId]: [
|
||||||
{
|
{
|
||||||
email: 'owner2@email.com',
|
email: 'owner2@email.com',
|
||||||
imageUrl: 'image-url-3',
|
imageUrl: 'https://image-url-3',
|
||||||
name: 'Second Owner Name',
|
name: 'Second Owner Name',
|
||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: 'owner@email.com',
|
email: 'owner@email.com',
|
||||||
imageUrl: 'image-url-1',
|
imageUrl: 'https://image-url-1',
|
||||||
name: 'Owner Name',
|
name: 'Owner Name',
|
||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { Db } from '../../db/db';
|
import type { Db } from '../../db/db';
|
||||||
import { RoleName, type IProjectWithCount } from '../../types';
|
import { RoleName, type IProjectWithCount } from '../../types';
|
||||||
|
import { generateImageUrl } from '../../util';
|
||||||
import type {
|
import type {
|
||||||
GroupProjectOwner,
|
GroupProjectOwner,
|
||||||
IProjectOwnersReadModel,
|
IProjectOwnersReadModel,
|
||||||
@ -37,6 +38,7 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
|
|||||||
): Promise<Record<string, UserProjectOwner[]>> {
|
): Promise<Record<string, UserProjectOwner[]>> {
|
||||||
const usersResult = await this.db
|
const usersResult = await this.db
|
||||||
.select(
|
.select(
|
||||||
|
'user.id',
|
||||||
'user.username',
|
'user.username',
|
||||||
'user.name',
|
'user.name',
|
||||||
'user.email',
|
'user.email',
|
||||||
@ -58,7 +60,7 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
|
|||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
name: user?.name || user?.username,
|
name: user?.name || user?.username,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
imageUrl: user?.image_url,
|
imageUrl: generateImageUrl(user),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (project in usersDict) {
|
if (project in usersDict) {
|
||||||
|
Loading…
Reference in New Issue
Block a user