mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
chore: overhaul project list pages (#10447)
https://linear.app/unleash/issue/2-3743/overhaul-project-list-pages This is essentially scouting work on our project list pages so we have a better baseline before proceeding with the cards/list view toggle. Includes refactoring, fixes and visual improvements ensuring better consistency and alignment with our designs. Took some liberties, so feel free to tell me I'm wrong. ### Searching <img width="1224" height="667" alt="image" src="https://github.com/user-attachments/assets/3f1bf700-7323-4c00-81db-7b57d125810b" /> ### Search results only in one of the groups <img width="1216" height="384" alt="image" src="https://github.com/user-attachments/assets/f67536e3-42de-4371-9725-c38a6fe0889a" /> ### No results found <img width="1218" height="347" alt="image" src="https://github.com/user-attachments/assets/c15c3555-1f37-473e-8a3e-8a549bd24966" /> ### Helper text <img width="334" height="114" alt="image" src="https://github.com/user-attachments/assets/c9150c9c-22c6-4f73-8989-b9cba4b52793" /> ### Title truncation with tooltip <img width="333" height="192" alt="image" src="https://github.com/user-attachments/assets/1f88d82d-67b2-4327-9301-808fef1e11ac" /> ### Archived projects <img width="1075" height="351" alt="image" src="https://github.com/user-attachments/assets/87b10618-b7c4-4351-87d3-3e678ddd20ae" />
This commit is contained in:
parent
ddd503952b
commit
0ac997e63e
@ -1,10 +1,11 @@
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
StyledProjectCard,
|
||||
StyledBox,
|
||||
StyledCardTitle,
|
||||
StyledProjectCardTitle,
|
||||
StyledProjectCardBody,
|
||||
StyledActions,
|
||||
StyledProjectCardHeader,
|
||||
StyledProjectCardContent,
|
||||
StyledProjectCardTitleContainer,
|
||||
} from './ProjectCard.styles';
|
||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
|
||||
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
|
||||
@ -12,8 +13,8 @@ import type { ProjectSchemaOwners } from 'openapi';
|
||||
import { formatDateYMDHM } from 'utils/formatDate';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { Box, Link, styled, Tooltip } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Box, styled, Tooltip } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
DELETE_PROJECT,
|
||||
UPDATE_PROJECT,
|
||||
@ -24,7 +25,12 @@ import Delete from '@mui/icons-material/Delete';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { flexRow } from 'themes/themeStyles';
|
||||
import { Truncator } from 'component/common/Truncator/Truncator.tsx';
|
||||
|
||||
const StyledActions = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export type ProjectArchiveCardProps = {
|
||||
id: string;
|
||||
@ -36,24 +42,6 @@ export type ProjectArchiveCardProps = {
|
||||
owners?: ProjectSchemaOwners;
|
||||
};
|
||||
|
||||
export const StyledDivHeader = styled('div')(({ theme }) => ({
|
||||
...flexRow,
|
||||
width: '100%',
|
||||
gap: theme.spacing(1),
|
||||
minHeight: theme.spacing(6),
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledTitle = styled(StyledCardTitle)(({ theme }) => ({
|
||||
margin: 0,
|
||||
}));
|
||||
|
||||
const StyledContent = styled('div')(({ theme }) => ({
|
||||
...flexRow,
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
||||
id,
|
||||
name,
|
||||
@ -69,51 +57,48 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
||||
return (
|
||||
<StyledProjectCard disabled data-testid={id}>
|
||||
<StyledProjectCardBody>
|
||||
<StyledDivHeader>
|
||||
<StyledBox data-loading>
|
||||
<Tooltip title={`id: ${id}`} arrow>
|
||||
<StyledTitle>
|
||||
<Highlighter search={searchQuery}>
|
||||
{name}
|
||||
</Highlighter>
|
||||
</StyledTitle>
|
||||
</Tooltip>
|
||||
</StyledBox>
|
||||
<ProjectModeBadge mode={mode} />
|
||||
</StyledDivHeader>
|
||||
<StyledContent>
|
||||
<Tooltip
|
||||
title={
|
||||
archivedAt
|
||||
? formatDateYMDHM(
|
||||
parseISO(archivedAt as string),
|
||||
locationSettings.locale,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
arrow
|
||||
placement='top'
|
||||
>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
})}
|
||||
<StyledProjectCardHeader>
|
||||
<StyledProjectCardTitleContainer data-loading>
|
||||
<Truncator
|
||||
title={name}
|
||||
arrow
|
||||
component={StyledProjectCardTitle}
|
||||
>
|
||||
<p data-loading>
|
||||
Archived:{' '}
|
||||
<TimeAgo date={archivedAt} refresh={false} />
|
||||
</p>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/archive?search=project%3A${encodeURI(id)}`}
|
||||
>
|
||||
<p>View archived flags</p>
|
||||
</Link>
|
||||
</StyledContent>
|
||||
<Highlighter search={searchQuery}>
|
||||
{name}
|
||||
</Highlighter>
|
||||
</Truncator>
|
||||
</StyledProjectCardTitleContainer>
|
||||
<ProjectModeBadge mode={mode} />
|
||||
</StyledProjectCardHeader>
|
||||
<StyledProjectCardContent>
|
||||
{archivedAt && (
|
||||
<div data-loading>
|
||||
Archived{' '}
|
||||
<Tooltip
|
||||
title={formatDateYMDHM(
|
||||
parseISO(archivedAt as string),
|
||||
locationSettings.locale,
|
||||
)}
|
||||
arrow
|
||||
>
|
||||
<strong>
|
||||
<TimeAgo
|
||||
date={archivedAt}
|
||||
refresh={false}
|
||||
/>
|
||||
</strong>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div data-loading>
|
||||
<Link to={`/archive?search=project%3A${encodeURI(id)}`}>
|
||||
View archived flags
|
||||
</Link>
|
||||
</div>
|
||||
</StyledProjectCardContent>
|
||||
</StyledProjectCardBody>
|
||||
<ProjectCardFooter id={id} disabled owners={owners}>
|
||||
<ProjectCardFooter id={id} owners={owners}>
|
||||
<StyledActions>
|
||||
<PermissionIconButton
|
||||
onClick={onRevive}
|
||||
@ -121,6 +106,7 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
||||
permission={UPDATE_PROJECT}
|
||||
tooltipProps={{ title: 'Revive project' }}
|
||||
data-testid={`revive-feature-flag-button`}
|
||||
size='small'
|
||||
>
|
||||
<Undo />
|
||||
</PermissionIconButton>
|
||||
@ -129,6 +115,7 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
||||
projectId={id}
|
||||
tooltipProps={{ title: 'Permanently delete project' }}
|
||||
onClick={onDelete}
|
||||
size='small'
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
|
@ -1,8 +1,6 @@
|
||||
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 { flexRow } from 'themes/themeStyles';
|
||||
import { flexColumn, flexRow } from 'themes/themeStyles';
|
||||
|
||||
export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
|
||||
({ theme, disabled = false }) => ({
|
||||
@ -16,11 +14,11 @@ export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
|
||||
justifyContent: 'center',
|
||||
},
|
||||
transition: 'background-color 0.2s ease-in-out',
|
||||
backgroundColor: disabled
|
||||
? theme.palette.neutral.light
|
||||
: theme.palette.background.default,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
backgroundColor: disabled
|
||||
? theme.palette.background.default
|
||||
: theme.palette.action.hover,
|
||||
},
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
}),
|
||||
@ -33,63 +31,31 @@ export const StyledProjectCardBody = styled(Box)(({ theme }) => ({
|
||||
justifyContent: 'space-between',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
}));
|
||||
|
||||
export const StyledDivHeader = styled('div')(({ theme }) => ({
|
||||
...flexRow,
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const StyledCardTitle = styled('h3')<{ lines?: number }>(
|
||||
({ theme, lines = 2 }) => ({
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
lineClamp: `${lines}`,
|
||||
WebkitLineClamp: lines,
|
||||
lineHeight: '1.2',
|
||||
display: '-webkit-box',
|
||||
boxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
alignItems: 'flex-start',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
wordBreak: 'break-word',
|
||||
}),
|
||||
);
|
||||
|
||||
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 StyledDivInfo = styled('div')(({ theme }) => ({
|
||||
export const StyledProjectCardHeader = styled('div')(({ theme }) => ({
|
||||
gap: theme.spacing(1),
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
export const StyledProjectCardTitleContainer = styled('div')(({ theme }) => ({
|
||||
...flexColumn,
|
||||
margin: theme.spacing(1, 'auto', 1, 0),
|
||||
}));
|
||||
|
||||
export const StyledProjectCardTitle = styled('h3')(({ theme }) => ({
|
||||
margin: 0,
|
||||
marginRight: 'auto',
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
lineHeight: '1.2',
|
||||
}));
|
||||
|
||||
export const StyledProjectCardContent = styled('div')(({ theme }) => ({
|
||||
...flexRow,
|
||||
justifyContent: 'space-between',
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
padding: theme.spacing(0, 1),
|
||||
}));
|
||||
|
||||
export const StyledParagraphInfo = styled('p')<{ disabled?: boolean }>(
|
||||
({ theme, disabled = false }) => ({
|
||||
color: disabled ? 'inherit' : theme.palette.primary.dark,
|
||||
fontWeight: disabled ? 'normal' : 'bold',
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledActions = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
margin: theme.spacing(0.5),
|
||||
}));
|
||||
|
@ -1,13 +1,15 @@
|
||||
import {
|
||||
StyledProjectCardTitle,
|
||||
StyledProjectCard,
|
||||
StyledCardTitle,
|
||||
StyledProjectCardBody,
|
||||
StyledProjectCardHeader,
|
||||
StyledProjectCardContent,
|
||||
StyledProjectCardTitleContainer,
|
||||
} from './ProjectCard.styles';
|
||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
|
||||
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
|
||||
import { FavoriteAction } from './FavoriteAction/FavoriteAction.tsx';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { flexColumn, flexRow } from 'themes/themeStyles';
|
||||
import { styled } from '@mui/material';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen.tsx';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
@ -16,41 +18,18 @@ import { ProjectMembers } from './ProjectCardFooter/ProjectMembers/ProjectMember
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
||||
import type { ProjectSchema } from 'openapi';
|
||||
import { Truncator } from 'component/common/Truncator/Truncator.tsx';
|
||||
|
||||
const StyledUpdated = styled('span')(({ theme }) => ({
|
||||
const StyledSubtitle = styled('span')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
}));
|
||||
|
||||
const StyledCount = styled('strong')(({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
}));
|
||||
|
||||
const StyledInfo = styled('div')(({ theme }) => ({
|
||||
...flexColumn,
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
}));
|
||||
|
||||
const StyledContent = styled('div')({
|
||||
...flexRow,
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
gap: theme.spacing(1),
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
type ProjectCardProps = ProjectSchema & { onHover?: () => void };
|
||||
|
||||
export const ProjectCard = ({
|
||||
name,
|
||||
featureCount,
|
||||
health,
|
||||
memberCount = 0,
|
||||
onHover,
|
||||
id,
|
||||
mode,
|
||||
favorite = false,
|
||||
@ -58,47 +37,47 @@ export const ProjectCard = ({
|
||||
createdAt,
|
||||
lastUpdatedAt,
|
||||
lastReportedFlagUsage,
|
||||
}: ProjectCardProps) => {
|
||||
}: ProjectSchema) => {
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
return (
|
||||
<StyledProjectCard onMouseEnter={onHover}>
|
||||
<StyledProjectCard>
|
||||
<StyledProjectCardBody>
|
||||
<StyledHeader>
|
||||
<Box
|
||||
data-loading
|
||||
sx={(theme) => ({
|
||||
...flexColumn,
|
||||
margin: theme.spacing(1, 'auto', 1, 0),
|
||||
})}
|
||||
>
|
||||
<StyledCardTitle lines={1} sx={{ margin: 0 }}>
|
||||
<StyledProjectCardHeader>
|
||||
<StyledProjectCardTitleContainer data-loading>
|
||||
<Truncator
|
||||
title={name}
|
||||
arrow
|
||||
component={StyledProjectCardTitle}
|
||||
>
|
||||
<Highlighter search={searchQuery}>
|
||||
{name}
|
||||
</Highlighter>
|
||||
</StyledCardTitle>
|
||||
<StyledUpdated>
|
||||
</Truncator>
|
||||
<StyledSubtitle>
|
||||
Updated{' '}
|
||||
<TimeAgo date={lastUpdatedAt || createdAt} />
|
||||
</StyledUpdated>
|
||||
</Box>
|
||||
</StyledSubtitle>
|
||||
</StyledProjectCardTitleContainer>
|
||||
<ProjectModeBadge mode={mode} />
|
||||
<FavoriteAction id={id} isFavorite={favorite} />
|
||||
</StyledHeader>
|
||||
<StyledInfo>
|
||||
<div data-loading>
|
||||
<StyledCount>{featureCount}</StyledCount> flag
|
||||
{featureCount === 1 ? '' : 's'}
|
||||
</div>
|
||||
<StyledContent>
|
||||
</StyledProjectCardHeader>
|
||||
<div>
|
||||
<StyledProjectCardContent>
|
||||
<div data-loading>
|
||||
<StyledCount>{health}%</StyledCount> health
|
||||
<strong>{featureCount}</strong> flag
|
||||
{featureCount === 1 ? '' : 's'}
|
||||
</div>
|
||||
</StyledProjectCardContent>
|
||||
<StyledProjectCardContent>
|
||||
<div data-loading>
|
||||
<strong>{health}%</strong> health
|
||||
</div>
|
||||
<div data-loading>
|
||||
<ProjectLastSeen date={lastReportedFlagUsage} />
|
||||
</div>
|
||||
</StyledContent>
|
||||
</StyledInfo>
|
||||
</StyledProjectCardContent>
|
||||
</div>
|
||||
</StyledProjectCardBody>
|
||||
<ProjectCardFooter id={id} owners={owners}>
|
||||
<ConditionallyRender
|
||||
|
@ -11,35 +11,29 @@ interface IProjectCardFooterProps {
|
||||
id?: string;
|
||||
isFavorite?: boolean;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
owners?: IProjectOwnersProps['owners'];
|
||||
}
|
||||
|
||||
const StyledFooter = styled(Box)<{ disabled: boolean }>(
|
||||
({ theme, disabled }) => ({
|
||||
display: 'flex',
|
||||
background: disabled
|
||||
? theme.palette.background.paper
|
||||
: theme.palette.background.elevation1,
|
||||
boxShadow: theme.boxShadows.accordionFooter,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
paddingInline: theme.spacing(2),
|
||||
paddingBlock: theme.spacing(1.5),
|
||||
}),
|
||||
);
|
||||
const StyledFooter = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
background: theme.palette.background.elevation1,
|
||||
boxShadow: theme.boxShadows.accordionFooter,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
paddingInline: theme.spacing(2),
|
||||
paddingBlock: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
||||
children,
|
||||
owners,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const ownersWithoutSystem = owners?.filter(
|
||||
(owner) => owner.ownerType !== 'system',
|
||||
);
|
||||
return (
|
||||
<StyledFooter disabled={disabled}>
|
||||
<StyledFooter>
|
||||
{ownersWithoutSystem ? (
|
||||
<ProjectOwners
|
||||
owners={ownersWithoutSystem as ProjectSchemaOwners}
|
||||
|
@ -4,9 +4,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { ProjectCard as DefaultProjectCard } from '../ProjectCard/ProjectCard.tsx';
|
||||
import type { ProjectSchema } from 'openapi';
|
||||
import loadingData from './loadingData.ts';
|
||||
import { TablePlaceholder } from 'component/common/Table';
|
||||
import { styled } from '@mui/material';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { UpgradeProjectCard } from '../ProjectCard/UpgradeProjectCard.tsx';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
@ -40,86 +38,57 @@ type ProjectGroupProps = {
|
||||
export const ProjectGroup = ({
|
||||
projects,
|
||||
loading,
|
||||
placeholder = 'No projects available.',
|
||||
ProjectCardComponent,
|
||||
link = true,
|
||||
}: ProjectGroupProps) => {
|
||||
const ProjectCard = ProjectCardComponent ?? DefaultProjectCard;
|
||||
const { isOss } = useUiConfig();
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledGridContainer>
|
||||
<ConditionallyRender
|
||||
condition={projects.length < 1 && !loading}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={searchQuery?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No projects found matching “
|
||||
{searchQuery}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>{placeholder}</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<StyledGridContainer>
|
||||
<ConditionallyRender
|
||||
condition={loading}
|
||||
show={() => (
|
||||
<>
|
||||
{loadingData.map(
|
||||
(project: ProjectSchema) => (
|
||||
<ProjectCard
|
||||
data-loading
|
||||
createdAt={project.createdAt}
|
||||
key={project.id}
|
||||
name={project.name}
|
||||
id={project.id}
|
||||
mode={project.mode}
|
||||
memberCount={2}
|
||||
health={95}
|
||||
featureCount={4}
|
||||
owners={[
|
||||
{
|
||||
ownerType: 'user',
|
||||
name: 'Loading data',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
elseShow={() => (
|
||||
<>
|
||||
{projects.map((project) =>
|
||||
link ? (
|
||||
<StyledCardLink
|
||||
key={project.id}
|
||||
to={`/projects/${project.id}`}
|
||||
>
|
||||
<ProjectCard {...project} />
|
||||
</StyledCardLink>
|
||||
) : (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
{...project}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{isOss() ? <UpgradeProjectCard /> : null}
|
||||
</StyledGridContainer>
|
||||
}
|
||||
condition={loading}
|
||||
show={() => (
|
||||
<>
|
||||
{loadingData.map((project: ProjectSchema) => (
|
||||
<ProjectCard
|
||||
data-loading
|
||||
createdAt={project.createdAt}
|
||||
key={project.id}
|
||||
name={project.name}
|
||||
id={project.id}
|
||||
mode={project.mode}
|
||||
memberCount={2}
|
||||
health={95}
|
||||
featureCount={4}
|
||||
owners={[
|
||||
{
|
||||
ownerType: 'user',
|
||||
name: 'Loading data',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
elseShow={() => (
|
||||
<>
|
||||
{projects.map((project) =>
|
||||
link ? (
|
||||
<StyledCardLink
|
||||
key={project.id}
|
||||
to={`/projects/${project.id}`}
|
||||
>
|
||||
<ProjectCard {...project} />
|
||||
</StyledCardLink>
|
||||
) : (
|
||||
<ProjectCard key={project.id} {...project} />
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
{isOss() ? <UpgradeProjectCard /> : null}
|
||||
</StyledGridContainer>
|
||||
);
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort.ts';
|
||||
import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx';
|
||||
import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { TablePlaceholder } from 'component/common/Table/index.ts';
|
||||
|
||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||
maxWidth: '500px',
|
||||
@ -38,7 +39,7 @@ export const ProjectList = () => {
|
||||
|
||||
const [state, setState] = useProjectsListState();
|
||||
|
||||
const myProjects = new Set(useProfile().profile?.projects || []);
|
||||
const myProfileProjects = new Set(useProfile().profile?.projects || []);
|
||||
|
||||
const setSearchValue = useCallback(
|
||||
(value: string) => setState({ query: value || undefined }),
|
||||
@ -50,13 +51,20 @@ export const ProjectList = () => {
|
||||
state.query,
|
||||
state.sortBy,
|
||||
);
|
||||
const groupedProjects = useGroupedProjects(sortedProjects, myProjects);
|
||||
const groupedProjects = useGroupedProjects(
|
||||
sortedProjects,
|
||||
myProfileProjects,
|
||||
);
|
||||
|
||||
const projectCount =
|
||||
sortedProjects.length < projects.length
|
||||
? `${sortedProjects.length} of ${projects.length}`
|
||||
: projects.length;
|
||||
|
||||
const myProjects = isOss() ? sortedProjects : groupedProjects.myProjects;
|
||||
|
||||
const otherProjects = isOss() ? [] : groupedProjects.otherProjects;
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
isLoading={loading}
|
||||
@ -66,7 +74,7 @@ export const ProjectList = () => {
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={!isOss && !isSmallScreen}
|
||||
condition={!isOss() && !isSmallScreen}
|
||||
show={
|
||||
<>
|
||||
<Search
|
||||
@ -113,42 +121,61 @@ export const ProjectList = () => {
|
||||
)}
|
||||
/>
|
||||
<SearchHighlightProvider value={state.query || ''}>
|
||||
<div>
|
||||
<ProjectsListHeader
|
||||
subtitle='Favorite projects, projects you own, and projects you are a member of'
|
||||
actions={
|
||||
<ProjectsListSort
|
||||
sortBy={state.sortBy}
|
||||
setSortBy={(sortBy) =>
|
||||
setState({
|
||||
sortBy: sortBy as typeof state.sortBy,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
My projects
|
||||
</ProjectsListHeader>
|
||||
<ProjectGroup
|
||||
loading={loading}
|
||||
projects={
|
||||
isOss()
|
||||
? sortedProjects
|
||||
: groupedProjects.myProjects
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!isOss() ? (
|
||||
{myProjects.length > 0 && (
|
||||
<div>
|
||||
<ProjectsListHeader subtitle='Projects in Unleash that you have access to.'>
|
||||
<ProjectsListHeader
|
||||
helpText='Favorite projects, projects you own, and projects you are a member of'
|
||||
actions={
|
||||
<ProjectsListSort
|
||||
sortBy={state.sortBy}
|
||||
setSortBy={(sortBy) =>
|
||||
setState({
|
||||
sortBy: sortBy as typeof state.sortBy,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
My projects
|
||||
</ProjectsListHeader>
|
||||
<ProjectGroup
|
||||
loading={loading}
|
||||
projects={
|
||||
isOss()
|
||||
? sortedProjects
|
||||
: groupedProjects.myProjects
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{otherProjects.length > 0 && (
|
||||
<div>
|
||||
<ProjectsListHeader helpText='Projects in Unleash that you have access to.'>
|
||||
Other projects
|
||||
</ProjectsListHeader>
|
||||
<ProjectGroup
|
||||
loading={loading}
|
||||
projects={groupedProjects.otherProjects}
|
||||
projects={otherProjects}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
{!loading &&
|
||||
!myProjects.length &&
|
||||
!otherProjects.length && (
|
||||
<>
|
||||
{state.query?.length ? (
|
||||
<TablePlaceholder>
|
||||
No projects found matching “
|
||||
{state.query}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
) : (
|
||||
<TablePlaceholder>
|
||||
No projects available.
|
||||
</TablePlaceholder>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SearchHighlightProvider>
|
||||
</StyledContainer>
|
||||
</PageContent>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { styled, Typography } from '@mui/material';
|
||||
import { styled } from '@mui/material';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
type ProjectsListHeaderProps = {
|
||||
children?: ReactNode;
|
||||
subtitle?: string;
|
||||
children: ReactNode;
|
||||
helpText: string;
|
||||
actions?: ReactNode;
|
||||
};
|
||||
|
||||
@ -18,28 +19,22 @@ const StyledHeaderContainer = styled('div')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledHeaderTitle = styled('div')(() => ({
|
||||
const StyledHeaderTitle = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
flexGrow: 0,
|
||||
}));
|
||||
|
||||
export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({
|
||||
children,
|
||||
subtitle,
|
||||
helpText,
|
||||
actions,
|
||||
}) => {
|
||||
return (
|
||||
<StyledHeaderContainer>
|
||||
<StyledHeaderTitle>
|
||||
{children ? (
|
||||
<Typography component='h2' variant='h2'>
|
||||
{children}
|
||||
</Typography>
|
||||
) : null}
|
||||
{subtitle ? (
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : null}
|
||||
{children}
|
||||
<HelpIcon tooltip={helpText} />
|
||||
</StyledHeaderTitle>
|
||||
{actions}
|
||||
</StyledHeaderContainer>
|
||||
|
@ -9,7 +9,7 @@ const StyledWrapper = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const StyledContainer = styled('div')(() => ({
|
||||
maxWidth: '220px',
|
||||
maxWidth: '200px',
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user