1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

chore: remove flag for new project cards (#7225)

This PR removes the flag for the new project card design, making it GA.

It also removes deprecated components and updates one reference (in the
groups card) to the new components instead.
This commit is contained in:
Thomas Heartman 2024-05-31 10:58:31 +02:00 committed by GitHub
parent bea5929460
commit de74faac46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 13 additions and 395 deletions

View File

@ -2,12 +2,12 @@ import { styled, Tooltip } from '@mui/material';
import type { IGroup } from 'interfaces/group';
import { Link, useNavigate } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
import { Badge } from 'component/common/Badge/Badge';
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';
import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
import { GroupCardAvatars } from './GroupCardAvatars/NewGroupCardAvatars';
const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',

View File

@ -1,82 +0,0 @@
import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import type { IGroupUser } from 'interfaces/group';
import type React from 'react';
import { useMemo, useState } from 'react';
import { GroupPopover } from './GroupPopover/GroupPopover';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
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,
},
}));
interface IGroupCardAvatarsProps {
users: IGroupUser[];
}
/**
* @deprecated Remove after with `projectsListNewCards` flag
*/
export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
const shownUsers = useMemo(
() =>
users
.sort((a, b) => b?.joinedAt!.getTime() - a?.joinedAt!.getTime())
.slice(0, 9),
[users],
);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [popupUser, setPopupUser] = useState<IGroupUser>();
const onPopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const onPopoverClose = () => {
setAnchorEl(null);
};
const avatarOpen = Boolean(anchorEl);
return (
<StyledAvatars>
{shownUsers.map((user) => (
<StyledAvatar
key={user.id}
user={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>
);
};

View File

@ -27,12 +27,6 @@ const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
},
}));
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,

View File

@ -1,83 +0,0 @@
import { styled } from '@mui/material';
import { Card, Box } from '@mui/material';
import Delete from '@mui/icons-material/Delete';
import Edit from '@mui/icons-material/Edit';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIcon.svg';
import { flexRow } from 'themes/themeStyles';
export const StyledProjectCard = styled(Card)(({ theme }) => ({
padding: theme.spacing(1, 2, 2, 2),
width: '220px',
height: '204px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
margin: theme.spacing(1),
boxShadow: 'none',
border: `1px solid ${theme.palette.divider}`,
[theme.breakpoints.down('sm')]: {
justifyContent: 'center',
},
'&:hover': {
transition: 'background-color 0.2s ease-in-out',
backgroundColor: theme.palette.neutral.light,
},
}));
export const StyledDivHeader = styled('div')(() => ({
...flexRow,
width: '100%',
}));
export const StyledH2Title = styled('h2')(({ theme }) => ({
fontWeight: 'normal',
fontSize: theme.fontSizes.bodySize,
lineClamp: 2,
display: '-webkit-box',
boxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden',
alignItems: 'flex-start',
}));
export const StyledBox = styled(Box)(() => ({
...flexRow,
marginRight: 'auto',
}));
export const StyledEditIcon = styled(Edit)(({ theme }) => ({
color: theme.palette.neutral.main,
marginRight: theme.spacing(1),
}));
export const StyledDeleteIcon = styled(Delete)(({ theme }) => ({
color: theme.palette.neutral.main,
marginRight: theme.spacing(1),
}));
export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
margin: theme.spacing(2, 'auto'),
width: '80px',
display: 'block',
fill: 'red',
}));
export const StyledDivInfo = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
fontSize: theme.fontSizes.smallerBody,
}));
export const StyledDivInfoContainer = styled('div')(() => ({
textAlign: 'center',
}));
export const StyledParagraphInfo = styled('p')(({ theme }) => ({
color: theme.palette.primary.dark,
fontWeight: 'bold',
}));
export const StyledIconBox = styled(Box)(() => ({
display: 'flex',
justifyContent: 'center',
}));

View File

@ -1,176 +0,0 @@
import { Menu, MenuItem } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import type React from 'react';
import { type SyntheticEvent, useContext, 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 AccessContext from 'contexts/AccessContext';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
import { DeleteProjectDialogue } from '../Project/DeleteProject/DeleteProjectDialogue';
import {
StyledProjectCard,
StyledDivHeader,
StyledBox,
StyledH2Title,
StyledEditIcon,
StyledProjectIcon,
StyledDivInfo,
StyledDivInfoContainer,
StyledParagraphInfo,
StyledIconBox,
} from './ProjectCard.styles';
import useToast from 'hooks/useToast';
import { HiddenProjectIconWithTooltip } from '../Project/HiddenProjectIconWithTooltip/HiddenProjectIconWithTooltip';
interface IProjectCardProps {
name: string;
featureCount: number;
health: number;
memberCount: number;
id: string;
onHover: () => void;
isFavorite?: boolean;
mode: string;
}
export const ProjectCard = ({
name,
featureCount,
health,
memberCount,
onHover,
id,
mode,
isFavorite = false,
}: IProjectCardProps) => {
const { hasAccess } = useContext(AccessContext);
const { setToastApiError } = useToast();
const { isOss } = useUiConfig();
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [showDelDialog, setShowDelDialog] = useState(false);
const navigate = useNavigate();
const { favorite, unfavorite } = useFavoriteProjectsApi();
const { refetch } = useProjects();
const handleClick = (event: React.SyntheticEvent) => {
event.preventDefault();
setAnchorEl(event.currentTarget);
};
const onFavorite = async (e: React.SyntheticEvent) => {
e.preventDefault();
try {
if (isFavorite) {
await unfavorite(id);
} else {
await favorite(id);
}
refetch();
} catch (error) {
setToastApiError('Something went wrong, could not update favorite');
}
};
return (
<StyledProjectCard onMouseEnter={onHover}>
<StyledDivHeader data-loading>
<StyledBox>
<FavoriteIconButton
onClick={onFavorite}
isFavorite={isFavorite}
size='medium'
sx={{ ml: -1 }}
/>
<StyledH2Title>{name}</StyledH2Title>
</StyledBox>
<PermissionIconButton
style={{ transform: 'translateX(7px)' }}
permission={UPDATE_PROJECT}
hidden={isOss()}
projectId={id}
data-loading
onClick={handleClick}
tooltipProps={{
title: 'Options',
}}
>
<MoreVertIcon />
</PermissionIconButton>
<Menu
id='project-card-menu'
open={Boolean(anchorEl)}
anchorEl={anchorEl}
style={{ top: 0, left: -100 }}
onClick={(event) => {
event.preventDefault();
}}
onClose={(event: SyntheticEvent) => {
event.preventDefault();
setAnchorEl(null);
}}
>
<MenuItem
onClick={(e) => {
e.preventDefault();
navigate(getProjectEditPath(id));
}}
>
<StyledEditIcon />
Edit project
</MenuItem>
</Menu>
</StyledDivHeader>
<StyledIconBox data-loading>
<ConditionallyRender
condition={mode === 'private'}
show={<HiddenProjectIconWithTooltip />}
elseShow={<StyledProjectIcon />}
/>
</StyledIconBox>
<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>
<DeleteProjectDialogue
project={id}
open={showDelDialog}
onClose={(e) => {
e.preventDefault();
setAnchorEl(null);
setShowDelDialog(false);
}}
/>
</StyledProjectCard>
);
};

View File

@ -1,23 +1,11 @@
import { Link } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ProjectCard as LegacyProjectCard } from '../ProjectCard/ProjectCard';
import { ProjectCard as NewProjectCard } from '../NewProjectCard/NewProjectCard';
import { ProjectCard } from '../NewProjectCard/NewProjectCard';
import type { IProjectCard } from 'interfaces/project';
import loadingData from './loadingData';
import { TablePlaceholder } from 'component/common/Table';
import { styled, Typography } from '@mui/material';
import { useUiFlag } from 'hooks/useUiFlag';
/**
* @deprecated Remove after with `projectsListNewCards` flag
*/
const StyledDivContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexWrap: 'wrap',
[theme.breakpoints.down('sm')]: {
justifyContent: 'center',
},
}));
const StyledGridContainer = styled('div')(({ theme }) => ({
display: 'grid',
@ -41,12 +29,6 @@ export const ProjectGroup: React.FC<{
loading: boolean;
searchValue: string;
}> = ({ sectionTitle, projects, loading, searchValue }) => {
const useNewProjectCards = useUiFlag('projectsListNewCards');
const [StyledItemsContainer, ProjectCard] = useNewProjectCards
? [StyledGridContainer, NewProjectCard]
: [StyledDivContainer, LegacyProjectCard];
return (
<article>
<ConditionallyRender
@ -81,7 +63,7 @@ export const ProjectGroup: React.FC<{
/>
}
elseShow={
<StyledItemsContainer>
<StyledGridContainer>
<ConditionallyRender
condition={loading}
show={() =>
@ -126,7 +108,7 @@ export const ProjectGroup: React.FC<{
</>
)}
/>
</StyledItemsContainer>
</StyledGridContainer>
}
/>
</article>

View File

@ -82,7 +82,6 @@ export type UiFlags = {
featureLifecycle?: boolean;
scimApi?: boolean;
createProjectWithEnvironmentConfig?: boolean;
projectsListNewCards?: boolean;
newCreateProjectUI?: boolean;
manyStrategiesPagination?: boolean;
enableLegacyVariants?: boolean;

View File

@ -141,7 +141,6 @@ exports[`should create default config 1`] = `
"parseProjectFromSession": false,
"personalAccessTokensKillSwitch": false,
"projectOverviewRefactorFeedback": false,
"projectsListNewCards": false,
"queryMissingTokens": false,
"responseTimeMetricsFix": false,
"responseTimeWithAppNameKillSwitch": false,

View File

@ -223,7 +223,6 @@ export default class ProjectController extends Controller {
user.id,
);
if (this.flagResolver.isEnabled('projectsListNewCards')) {
const projectsWithOwners =
await this.projectService.addOwnersToProjects(projects);
@ -233,14 +232,6 @@ export default class ProjectController extends Controller {
projectsSchema.$id,
{ version: 1, projects: serializeDates(projectsWithOwners) },
);
} else {
this.openApiService.respondWithValidation(
200,
res,
projectsSchema.$id,
{ version: 1, projects: serializeDates(projects) },
);
}
}
async getDeprecatedProjectOverview(

View File

@ -55,7 +55,6 @@ export type IFlagKey =
| 'projectOverviewRefactorFeedback'
| 'featureLifecycle'
| 'featureLifecycleMetrics'
| 'projectsListNewCards'
| 'parseProjectFromSession'
| 'createProjectWithEnvironmentConfig'
| 'manyStrategiesPagination'
@ -275,10 +274,6 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_CREATE_PROJECT_WITH_ENVIRONMENT_CONFIG,
false,
),
projectsListNewCards: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PROJECTS_LIST_NEW_CARDS,
false,
),
newCreateProjectUI: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_NEW_CREATE_PROJECT_UI,
false,

View File

@ -49,7 +49,6 @@ process.nextTick(async () => {
disableShowContextFieldSelectionValues: false,
projectOverviewRefactorFeedback: true,
featureLifecycle: true,
projectsListNewCards: true,
parseProjectFromSession: true,
createProjectWithEnvironmentConfig: true,
manyStrategiesPagination: true,