1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-13 11:17:26 +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:
Nuno Góis 2025-08-01 09:46:48 +01:00 committed by GitHub
parent ddd503952b
commit 0ac997e63e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 236 additions and 319 deletions

View File

@ -1,10 +1,11 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { import {
StyledProjectCard, StyledProjectCard,
StyledBox, StyledProjectCardTitle,
StyledCardTitle,
StyledProjectCardBody, StyledProjectCardBody,
StyledActions, StyledProjectCardHeader,
StyledProjectCardContent,
StyledProjectCardTitleContainer,
} from './ProjectCard.styles'; } from './ProjectCard.styles';
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx'; import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx'; import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
@ -12,8 +13,8 @@ import type { ProjectSchemaOwners } from 'openapi';
import { formatDateYMDHM } from 'utils/formatDate'; import { formatDateYMDHM } from 'utils/formatDate';
import { useLocationSettings } from 'hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { Box, Link, styled, Tooltip } from '@mui/material'; import { Box, styled, Tooltip } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
DELETE_PROJECT, DELETE_PROJECT,
UPDATE_PROJECT, UPDATE_PROJECT,
@ -24,7 +25,12 @@ import Delete from '@mui/icons-material/Delete';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; 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 = { export type ProjectArchiveCardProps = {
id: string; id: string;
@ -36,24 +42,6 @@ export type ProjectArchiveCardProps = {
owners?: ProjectSchemaOwners; 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> = ({ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
id, id,
name, name,
@ -69,51 +57,48 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
return ( return (
<StyledProjectCard disabled data-testid={id}> <StyledProjectCard disabled data-testid={id}>
<StyledProjectCardBody> <StyledProjectCardBody>
<StyledDivHeader> <StyledProjectCardHeader>
<StyledBox data-loading> <StyledProjectCardTitleContainer data-loading>
<Tooltip title={`id: ${id}`} arrow> <Truncator
<StyledTitle> title={name}
<Highlighter search={searchQuery}> arrow
{name} component={StyledProjectCardTitle}
</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,
})}
> >
<p data-loading> <Highlighter search={searchQuery}>
Archived:{' '} {name}
<TimeAgo date={archivedAt} refresh={false} /> </Highlighter>
</p> </Truncator>
</Box> </StyledProjectCardTitleContainer>
</Tooltip> <ProjectModeBadge mode={mode} />
<Link </StyledProjectCardHeader>
component={RouterLink} <StyledProjectCardContent>
to={`/archive?search=project%3A${encodeURI(id)}`} {archivedAt && (
> <div data-loading>
<p>View archived flags</p> Archived{' '}
</Link> <Tooltip
</StyledContent> 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> </StyledProjectCardBody>
<ProjectCardFooter id={id} disabled owners={owners}> <ProjectCardFooter id={id} owners={owners}>
<StyledActions> <StyledActions>
<PermissionIconButton <PermissionIconButton
onClick={onRevive} onClick={onRevive}
@ -121,6 +106,7 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
permission={UPDATE_PROJECT} permission={UPDATE_PROJECT}
tooltipProps={{ title: 'Revive project' }} tooltipProps={{ title: 'Revive project' }}
data-testid={`revive-feature-flag-button`} data-testid={`revive-feature-flag-button`}
size='small'
> >
<Undo /> <Undo />
</PermissionIconButton> </PermissionIconButton>
@ -129,6 +115,7 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
projectId={id} projectId={id}
tooltipProps={{ title: 'Permanently delete project' }} tooltipProps={{ title: 'Permanently delete project' }}
onClick={onDelete} onClick={onDelete}
size='small'
> >
<Delete /> <Delete />
</PermissionIconButton> </PermissionIconButton>

View File

@ -1,8 +1,6 @@
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { Card, Box } from '@mui/material'; import { Card, Box } from '@mui/material';
import Delete from '@mui/icons-material/Delete'; import { flexColumn, flexRow } from 'themes/themeStyles';
import Edit from '@mui/icons-material/Edit';
import { flexRow } from 'themes/themeStyles';
export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>( export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
({ theme, disabled = false }) => ({ ({ theme, disabled = false }) => ({
@ -16,11 +14,11 @@ export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
justifyContent: 'center', justifyContent: 'center',
}, },
transition: 'background-color 0.2s ease-in-out', transition: 'background-color 0.2s ease-in-out',
backgroundColor: disabled backgroundColor: theme.palette.background.default,
? theme.palette.neutral.light
: theme.palette.background.default,
'&:hover': { '&:hover': {
backgroundColor: theme.palette.neutral.light, backgroundColor: disabled
? theme.palette.background.default
: theme.palette.action.hover,
}, },
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
}), }),
@ -33,63 +31,31 @@ export const StyledProjectCardBody = styled(Box)(({ theme }) => ({
justifyContent: 'space-between', justifyContent: 'space-between',
height: '100%', height: '100%',
position: 'relative', position: 'relative',
}));
export const StyledDivHeader = styled('div')(({ theme }) => ({
...flexRow,
width: '100%',
marginBottom: theme.spacing(2),
gap: theme.spacing(1), gap: theme.spacing(1),
})); }));
export const StyledCardTitle = styled('h3')<{ lines?: number }>( export const StyledProjectCardHeader = styled('div')(({ theme }) => ({
({ theme, lines = 2 }) => ({ gap: theme.spacing(1),
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 }) => ({
display: 'flex', 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', justifyContent: 'space-between',
fontSize: theme.fontSizes.smallerBody, 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),
})); }));

View File

@ -1,13 +1,15 @@
import { import {
StyledProjectCardTitle,
StyledProjectCard, StyledProjectCard,
StyledCardTitle,
StyledProjectCardBody, StyledProjectCardBody,
StyledProjectCardHeader,
StyledProjectCardContent,
StyledProjectCardTitleContainer,
} from './ProjectCard.styles'; } from './ProjectCard.styles';
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx'; import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx'; import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
import { FavoriteAction } from './FavoriteAction/FavoriteAction.tsx'; import { FavoriteAction } from './FavoriteAction/FavoriteAction.tsx';
import { Box, styled } from '@mui/material'; import { styled } from '@mui/material';
import { flexColumn, flexRow } from 'themes/themeStyles';
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen.tsx'; import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen.tsx';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId'; import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import type { ProjectSchema } from 'openapi'; 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, color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody, 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 = ({ export const ProjectCard = ({
name, name,
featureCount, featureCount,
health, health,
memberCount = 0, memberCount = 0,
onHover,
id, id,
mode, mode,
favorite = false, favorite = false,
@ -58,47 +37,47 @@ export const ProjectCard = ({
createdAt, createdAt,
lastUpdatedAt, lastUpdatedAt,
lastReportedFlagUsage, lastReportedFlagUsage,
}: ProjectCardProps) => { }: ProjectSchema) => {
const { searchQuery } = useSearchHighlightContext(); const { searchQuery } = useSearchHighlightContext();
return ( return (
<StyledProjectCard onMouseEnter={onHover}> <StyledProjectCard>
<StyledProjectCardBody> <StyledProjectCardBody>
<StyledHeader> <StyledProjectCardHeader>
<Box <StyledProjectCardTitleContainer data-loading>
data-loading <Truncator
sx={(theme) => ({ title={name}
...flexColumn, arrow
margin: theme.spacing(1, 'auto', 1, 0), component={StyledProjectCardTitle}
})} >
>
<StyledCardTitle lines={1} sx={{ margin: 0 }}>
<Highlighter search={searchQuery}> <Highlighter search={searchQuery}>
{name} {name}
</Highlighter> </Highlighter>
</StyledCardTitle> </Truncator>
<StyledUpdated> <StyledSubtitle>
Updated{' '} Updated{' '}
<TimeAgo date={lastUpdatedAt || createdAt} /> <TimeAgo date={lastUpdatedAt || createdAt} />
</StyledUpdated> </StyledSubtitle>
</Box> </StyledProjectCardTitleContainer>
<ProjectModeBadge mode={mode} /> <ProjectModeBadge mode={mode} />
<FavoriteAction id={id} isFavorite={favorite} /> <FavoriteAction id={id} isFavorite={favorite} />
</StyledHeader> </StyledProjectCardHeader>
<StyledInfo> <div>
<div data-loading> <StyledProjectCardContent>
<StyledCount>{featureCount}</StyledCount> flag
{featureCount === 1 ? '' : 's'}
</div>
<StyledContent>
<div data-loading> <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>
<div data-loading> <div data-loading>
<ProjectLastSeen date={lastReportedFlagUsage} /> <ProjectLastSeen date={lastReportedFlagUsage} />
</div> </div>
</StyledContent> </StyledProjectCardContent>
</StyledInfo> </div>
</StyledProjectCardBody> </StyledProjectCardBody>
<ProjectCardFooter id={id} owners={owners}> <ProjectCardFooter id={id} owners={owners}>
<ConditionallyRender <ConditionallyRender

View File

@ -11,35 +11,29 @@ interface IProjectCardFooterProps {
id?: string; id?: string;
isFavorite?: boolean; isFavorite?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
disabled?: boolean;
owners?: IProjectOwnersProps['owners']; owners?: IProjectOwnersProps['owners'];
} }
const StyledFooter = styled(Box)<{ disabled: boolean }>( const StyledFooter = styled(Box)(({ theme }) => ({
({ theme, disabled }) => ({ display: 'flex',
display: 'flex', background: theme.palette.background.elevation1,
background: disabled boxShadow: theme.boxShadows.accordionFooter,
? theme.palette.background.paper alignItems: 'center',
: theme.palette.background.elevation1, justifyContent: 'space-between',
boxShadow: theme.boxShadows.accordionFooter, borderTop: `1px solid ${theme.palette.divider}`,
alignItems: 'center', paddingInline: theme.spacing(2),
justifyContent: 'space-between', paddingBlock: theme.spacing(1.5),
borderTop: `1px solid ${theme.palette.divider}`, }));
paddingInline: theme.spacing(2),
paddingBlock: theme.spacing(1.5),
}),
);
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({ export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
children, children,
owners, owners,
disabled = false,
}) => { }) => {
const ownersWithoutSystem = owners?.filter( const ownersWithoutSystem = owners?.filter(
(owner) => owner.ownerType !== 'system', (owner) => owner.ownerType !== 'system',
); );
return ( return (
<StyledFooter disabled={disabled}> <StyledFooter>
{ownersWithoutSystem ? ( {ownersWithoutSystem ? (
<ProjectOwners <ProjectOwners
owners={ownersWithoutSystem as ProjectSchemaOwners} owners={ownersWithoutSystem as ProjectSchemaOwners}

View File

@ -4,9 +4,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { ProjectCard as DefaultProjectCard } from '../ProjectCard/ProjectCard.tsx'; import { ProjectCard as DefaultProjectCard } from '../ProjectCard/ProjectCard.tsx';
import type { ProjectSchema } from 'openapi'; import type { ProjectSchema } from 'openapi';
import loadingData from './loadingData.ts'; import loadingData from './loadingData.ts';
import { TablePlaceholder } from 'component/common/Table';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { UpgradeProjectCard } from '../ProjectCard/UpgradeProjectCard.tsx'; import { UpgradeProjectCard } from '../ProjectCard/UpgradeProjectCard.tsx';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -40,86 +38,57 @@ type ProjectGroupProps = {
export const ProjectGroup = ({ export const ProjectGroup = ({
projects, projects,
loading, loading,
placeholder = 'No projects available.',
ProjectCardComponent, ProjectCardComponent,
link = true, link = true,
}: ProjectGroupProps) => { }: ProjectGroupProps) => {
const ProjectCard = ProjectCardComponent ?? DefaultProjectCard; const ProjectCard = ProjectCardComponent ?? DefaultProjectCard;
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const { searchQuery } = useSearchHighlightContext();
return ( return (
<> <StyledGridContainer>
<ConditionallyRender <ConditionallyRender
condition={projects.length < 1 && !loading} condition={loading}
show={ show={() => (
<ConditionallyRender <>
condition={searchQuery?.length > 0} {loadingData.map((project: ProjectSchema) => (
show={ <ProjectCard
<TablePlaceholder> data-loading
No projects found matching &ldquo; createdAt={project.createdAt}
{searchQuery} key={project.id}
&rdquo; name={project.name}
</TablePlaceholder> id={project.id}
} mode={project.mode}
elseShow={ memberCount={2}
<TablePlaceholder>{placeholder}</TablePlaceholder> health={95}
} featureCount={4}
/> owners={[
} {
elseShow={ ownerType: 'user',
<StyledGridContainer> name: 'Loading data',
<ConditionallyRender },
condition={loading} ]}
show={() => ( />
<> ))}
{loadingData.map( </>
(project: ProjectSchema) => ( )}
<ProjectCard elseShow={() => (
data-loading <>
createdAt={project.createdAt} {projects.map((project) =>
key={project.id} link ? (
name={project.name} <StyledCardLink
id={project.id} key={project.id}
mode={project.mode} to={`/projects/${project.id}`}
memberCount={2} >
health={95} <ProjectCard {...project} />
featureCount={4} </StyledCardLink>
owners={[ ) : (
{ <ProjectCard key={project.id} {...project} />
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>
}
/> />
</> {isOss() ? <UpgradeProjectCard /> : null}
</StyledGridContainer>
); );
}; };

View File

@ -18,6 +18,7 @@ import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort.ts';
import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx'; import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx';
import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx'; import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { TablePlaceholder } from 'component/common/Table/index.ts';
const StyledApiError = styled(ApiError)(({ theme }) => ({ const StyledApiError = styled(ApiError)(({ theme }) => ({
maxWidth: '500px', maxWidth: '500px',
@ -38,7 +39,7 @@ export const ProjectList = () => {
const [state, setState] = useProjectsListState(); const [state, setState] = useProjectsListState();
const myProjects = new Set(useProfile().profile?.projects || []); const myProfileProjects = new Set(useProfile().profile?.projects || []);
const setSearchValue = useCallback( const setSearchValue = useCallback(
(value: string) => setState({ query: value || undefined }), (value: string) => setState({ query: value || undefined }),
@ -50,13 +51,20 @@ export const ProjectList = () => {
state.query, state.query,
state.sortBy, state.sortBy,
); );
const groupedProjects = useGroupedProjects(sortedProjects, myProjects); const groupedProjects = useGroupedProjects(
sortedProjects,
myProfileProjects,
);
const projectCount = const projectCount =
sortedProjects.length < projects.length sortedProjects.length < projects.length
? `${sortedProjects.length} of ${projects.length}` ? `${sortedProjects.length} of ${projects.length}`
: projects.length; : projects.length;
const myProjects = isOss() ? sortedProjects : groupedProjects.myProjects;
const otherProjects = isOss() ? [] : groupedProjects.otherProjects;
return ( return (
<PageContent <PageContent
isLoading={loading} isLoading={loading}
@ -66,7 +74,7 @@ export const ProjectList = () => {
actions={ actions={
<> <>
<ConditionallyRender <ConditionallyRender
condition={!isOss && !isSmallScreen} condition={!isOss() && !isSmallScreen}
show={ show={
<> <>
<Search <Search
@ -113,42 +121,61 @@ export const ProjectList = () => {
)} )}
/> />
<SearchHighlightProvider value={state.query || ''}> <SearchHighlightProvider value={state.query || ''}>
<div> {myProjects.length > 0 && (
<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() ? (
<div> <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 Other projects
</ProjectsListHeader> </ProjectsListHeader>
<ProjectGroup <ProjectGroup
loading={loading} loading={loading}
projects={groupedProjects.otherProjects} projects={otherProjects}
/> />
</div> </div>
) : null} )}
{!loading &&
!myProjects.length &&
!otherProjects.length && (
<>
{state.query?.length ? (
<TablePlaceholder>
No projects found matching &ldquo;
{state.query}
&rdquo;
</TablePlaceholder>
) : (
<TablePlaceholder>
No projects available.
</TablePlaceholder>
)}
</>
)}
</SearchHighlightProvider> </SearchHighlightProvider>
</StyledContainer> </StyledContainer>
</PageContent> </PageContent>

View File

@ -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'; import type { FC, ReactNode } from 'react';
type ProjectsListHeaderProps = { type ProjectsListHeaderProps = {
children?: ReactNode; children: ReactNode;
subtitle?: string; helpText: string;
actions?: ReactNode; actions?: ReactNode;
}; };
@ -18,28 +19,22 @@ const StyledHeaderContainer = styled('div')(({ theme }) => ({
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
})); }));
const StyledHeaderTitle = styled('div')(() => ({ const StyledHeaderTitle = styled('div')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
flexGrow: 0, flexGrow: 0,
})); }));
export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({ export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({
children, children,
subtitle, helpText,
actions, actions,
}) => { }) => {
return ( return (
<StyledHeaderContainer> <StyledHeaderContainer>
<StyledHeaderTitle> <StyledHeaderTitle>
{children ? ( {children}
<Typography component='h2' variant='h2'> <HelpIcon tooltip={helpText} />
{children}
</Typography>
) : null}
{subtitle ? (
<Typography variant='body2' color='text.secondary'>
{subtitle}
</Typography>
) : null}
</StyledHeaderTitle> </StyledHeaderTitle>
{actions} {actions}
</StyledHeaderContainer> </StyledHeaderContainer>

View File

@ -9,7 +9,7 @@ const StyledWrapper = styled('div')(({ theme }) => ({
})); }));
const StyledContainer = styled('div')(() => ({ const StyledContainer = styled('div')(() => ({
maxWidth: '220px', maxWidth: '200px',
width: '100%', width: '100%',
})); }));