From 07abb609668c4d657cc0730a9c84bdc7ff7bb3c4 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:39:44 +0200 Subject: [PATCH] Feat/improve projects list (#8018) --- .../project/ProjectCard/ProjectCard.tsx | 50 +++++++------ .../ProjectOwners/ProjectOwners.tsx | 12 ++-- .../ProjectArchiveLink/ProjectArchiveLink.tsx | 38 ++++++++++ .../project/ProjectList/ProjectGroup.tsx | 72 +++++++++++++++---- .../project/ProjectList/ProjectList.tsx | 37 +++++----- .../ProjectsListSort/ProjectsListSort.tsx | 4 +- .../hooks/useProjectsSearchAndSort.test.ts | 31 ++++++++ .../hooks/useProjectsSearchAndSort.ts | 13 ++-- 8 files changed, 188 insertions(+), 69 deletions(-) create mode 100644 frontend/src/component/project/ProjectList/ProjectArchiveLink/ProjectArchiveLink.tsx diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.tsx b/frontend/src/component/project/ProjectCard/ProjectCard.tsx index b03d01f69a..0a4745c667 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectCard.tsx @@ -1,7 +1,5 @@ -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { StyledProjectCard, - StyledDivHeader, StyledCardTitle, StyledProjectCardBody, StyledIconBox, @@ -28,11 +26,19 @@ const StyledCount = styled('strong')(({ theme }) => ({ })); const StyledInfo = styled('div')(({ theme }) => ({ - display: 'flex', + display: 'grid', + gridTemplate: '1rem 1rem / 1fr 1fr', + gridAutoFlow: 'column', + alignItems: 'center', justifyContent: 'space-between', marginTop: theme.spacing(1), fontSize: theme.fontSizes.smallerBody, - alignItems: 'flex-end', +})); + +const StyledHeader = styled('div')(({ theme }) => ({ + gap: theme.spacing(1), + display: 'flex', + width: '100%', })); export const ProjectCard = ({ @@ -45,6 +51,7 @@ export const ProjectCard = ({ mode, favorite = false, owners, + createdAt, lastUpdatedAt, lastReportedFlagUsage, }: IProjectCard) => { @@ -53,7 +60,7 @@ export const ProjectCard = ({ return ( - + @@ -69,29 +76,26 @@ export const ProjectCard = ({ {name} - - Updated - - } - /> + + Updated{' '} + + - + -
-
- {featureCount} flag - {featureCount === 1 ? '' : 's'} -
-
- {health}% health -
+
+ {featureCount} flag + {featureCount === 1 ? '' : 's'} +
+
+ {health}% health +
+
+
+
- ({ const StyledContainer = styled('div')(() => ({ display: 'flex', flexDirection: 'column', + borderRadius: '50%', })); const StyledOwnerName = styled('div')(({ theme }) => ({ @@ -74,6 +75,7 @@ const StyledHeader = styled('span')(({ theme }) => ({ fontSize: theme.fontSizes.smallerBody, color: theme.palette.text.secondary, fontWeight: theme.typography.fontWeightRegular, + marginRight: 'auto', })); const StyledWrapper = styled('div')(({ theme }) => ({ @@ -92,10 +94,10 @@ export const ProjectOwners: FC = ({ owners = [] }) => { return ( - + @@ -103,8 +105,10 @@ export const ProjectOwners: FC = ({ owners = [] }) => { condition={owners.length === 1} show={ - Owner - {users[0]?.name} + Owner + + {users[0]?.name} + } /> diff --git a/frontend/src/component/project/ProjectList/ProjectArchiveLink/ProjectArchiveLink.tsx b/frontend/src/component/project/ProjectList/ProjectArchiveLink/ProjectArchiveLink.tsx new file mode 100644 index 0000000000..2d43b7ebf4 --- /dev/null +++ b/frontend/src/component/project/ProjectList/ProjectArchiveLink/ProjectArchiveLink.tsx @@ -0,0 +1,38 @@ +import { + IconButton, + Link, + Tooltip, + useMediaQuery, + useTheme, +} from '@mui/material'; +import ArchiveIcon from '@mui/icons-material/Inventory2Outlined'; +import { Link as RouterLink, useNavigate } from 'react-router-dom'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import type { FC } from 'react'; + +export const ProjectArchiveLink: FC = () => { + const navigate = useNavigate(); + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); + + if (isSmallScreen) { + return ( + + navigate('/projects-archive')} + data-loading + > + + + + ); + } + return ( + <> + + Archived projects + + + + ); +}; diff --git a/frontend/src/component/project/ProjectList/ProjectGroup.tsx b/frontend/src/component/project/ProjectList/ProjectGroup.tsx index 3799d9179e..bdda309e85 100644 --- a/frontend/src/component/project/ProjectList/ProjectGroup.tsx +++ b/frontend/src/component/project/ProjectList/ProjectGroup.tsx @@ -1,4 +1,4 @@ -import type { ComponentType } from 'react'; +import type { ComponentType, ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ProjectCard as LegacyProjectCard } from '../ProjectCard/LegacyProjectCard'; @@ -10,6 +10,26 @@ import { TablePlaceholder } from 'component/common/Table'; import { styled, Typography } from '@mui/material'; import { useUiFlag } from 'hooks/useUiFlag'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { flexColumn } from 'themes/themeStyles'; + +const StyledContainer = styled('article')(({ theme }) => ({ + ...flexColumn, + gap: theme.spacing(2), +})); + +const StyledHeaderContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column-reverse', + gap: theme.spacing(2), + [theme.breakpoints.up('md')]: { + flexDirection: 'row', + alignItems: 'flex-end', + }, +})); + +const StyledHeaderTitle = styled('div')(() => ({ + flexGrow: 0, +})); const StyledGridContainer = styled('div')(({ theme }) => ({ display: 'grid', @@ -29,6 +49,8 @@ const StyledCardLink = styled(Link)(({ theme }) => ({ type ProjectGroupProps = { sectionTitle?: string; + sectionSubtitle?: string; + HeaderActions?: ReactNode; projects: IProjectCard[]; loading: boolean; /** @@ -42,6 +64,8 @@ type ProjectGroupProps = { export const ProjectGroup = ({ sectionTitle, + sectionSubtitle, + HeaderActions, projects, loading, searchValue, @@ -56,19 +80,31 @@ export const ProjectGroup = ({ const { searchQuery } = useSearchHighlightContext(); return ( -
- ({ marginBottom: theme.spacing(2) })} - > - {sectionTitle} - - } - /> + + + + + {sectionTitle} + + } + /> + + {sectionSubtitle} + + } + /> + + {HeaderActions} + ), )} @@ -132,6 +174,6 @@ export const ProjectGroup = ({ } /> -
+
); }; diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index ac890ab3d7..4c3acbb40f 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -4,8 +4,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import ApiError from 'component/common/ApiError/ApiError'; -import { Link, styled, useMediaQuery } from '@mui/material'; -import { Link as RouterLink } from 'react-router-dom'; +import { styled, useMediaQuery } from '@mui/material'; import theme from 'themes/theme'; import { Search } from 'component/common/Search/Search'; import { useProfile } from 'hooks/api/getters/useProfile/useProfile'; @@ -18,6 +17,7 @@ import { ProjectList as LegacyProjectList } from './LegacyProjectList'; import { ProjectCreationButton } from './ProjectCreationButton/ProjectCreationButton'; import { useGroupedProjects } from './hooks/useGroupedProjects'; import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort'; +import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink'; const StyledApiError = styled(ApiError)(({ theme }) => ({ maxWidth: '500px', @@ -27,7 +27,7 @@ const StyledApiError = styled(ApiError)(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({ display: 'flex', flexDirection: 'column', - gap: theme.spacing(4), + gap: theme.spacing(6), })); const NewProjectList = () => { @@ -77,19 +77,10 @@ const NewProjectList = () => { } /> + - - Archived projects - - - - } + show={} /> { /> )} /> - - setState({ sortBy: sortBy as typeof state.sortBy }) - } - /> + setState({ + sortBy: sortBy as typeof state.sortBy, + }) + } + /> + } loading={loading} projects={groupedProjects.myProjects} /> diff --git a/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx b/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx index daafbe0dbb..f3c9b11158 100644 --- a/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx +++ b/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx @@ -5,7 +5,7 @@ import { styled } from '@mui/material'; const StyledWrapper = styled('div')(({ theme }) => ({ display: 'flex', justifyContent: 'flex-end', - margin: theme.spacing(0, 0, -4, 0), + flex: 1, })); const StyledContainer = styled('div')(() => ({ @@ -21,7 +21,7 @@ const options: Array<{ }> = [ { key: 'name', label: 'Project name' }, { key: 'created', label: 'Recently created' }, - { key: 'updated', label: 'Recently updated' }, + { key: 'updated', label: 'Last updated' }, { key: 'seen', label: 'Last usage reported' }, ]; diff --git a/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.test.ts b/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.test.ts index 5e4b6854b9..1510e7cf24 100644 --- a/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.test.ts +++ b/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.test.ts @@ -219,4 +219,35 @@ describe('useProjectsSearchAndSort', () => { 'Project A', ]); }); + + it('should use createdAt if lastUpdatedAt is not available', () => { + const { result } = renderHook( + (sortBy: string) => + useProjectsSearchAndSort( + [ + { + name: 'Project A', + id: '1', + createdAt: '2024-01-01', + lastUpdatedAt: '2024-01-02', + }, + { + name: 'Project B', + id: '2', + createdAt: '2024-02-01', + }, + ], + undefined, + sortBy as any, + ), + { + initialProps: 'updated', + }, + ); + + expect(result.current.map((project) => project.name)).toEqual([ + 'Project B', + 'Project A', + ]); + }); }); diff --git a/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.ts b/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.ts index fd5ccbdc19..a0864d118e 100644 --- a/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.ts +++ b/frontend/src/component/project/ProjectList/hooks/useProjectsSearchAndSort.ts @@ -15,6 +15,11 @@ export const useProjectsSearchAndSort = ( ? projects.filter((project) => regExp.test(project.name)) : projects ) + .sort((a, b) => { + const aVal = `${a.name || ''}`.toLowerCase(); + const bVal = `${b.name || ''}`.toLowerCase(); + return aVal?.localeCompare(bVal); + }) .sort((a, b) => { if (sortBy === 'created') { const aVal = new Date(a.createdAt || 0); @@ -23,8 +28,8 @@ export const useProjectsSearchAndSort = ( } if (sortBy === 'updated') { - const aVal = new Date(a.lastUpdatedAt || 0); - const bVal = new Date(b.lastUpdatedAt || 0); + const aVal = new Date(a.lastUpdatedAt || a.createdAt || 0); + const bVal = new Date(b.lastUpdatedAt || b.createdAt || 0); return bVal?.getTime() - aVal?.getTime(); } @@ -34,9 +39,7 @@ export const useProjectsSearchAndSort = ( return bVal?.getTime() - aVal?.getTime(); } - const aVal = `${a.name || ''}`.toLowerCase(); - const bVal = `${b.name || ''}`.toLowerCase(); - return aVal?.localeCompare(bVal); + return 0; }) .sort((a, b) => { if (a?.favorite && !b?.favorite) {