diff --git a/frontend/src/component/common/PageHeader/PageHeader.tsx b/frontend/src/component/common/PageHeader/PageHeader.tsx index 33282b557f5e..f96792e0555d 100644 --- a/frontend/src/component/common/PageHeader/PageHeader.tsx +++ b/frontend/src/component/common/PageHeader/PageHeader.tsx @@ -57,15 +57,6 @@ const StyledHeaderActions = styled('div')(({ theme }) => ({ gap: theme.spacing(1), })); -const StyledLeftHeaderActions = styled('div')(({ theme }) => ({ - display: 'flex', - flexGrow: 1, - justifyContent: 'flex-start', - alignItems: 'center', - gap: theme.spacing(1), - marginRight: theme.spacing(2), -})); - interface IPageHeaderProps { title?: string; titleElement?: ReactNode; @@ -73,7 +64,6 @@ interface IPageHeaderProps { variant?: TypographyProps['variant']; loading?: boolean; actions?: ReactNode; - leftActions?: ReactNode; className?: string; secondary?: boolean; } @@ -84,7 +74,6 @@ const PageHeaderComponent: FC & { title, titleElement, actions, - leftActions, subtitle, variant, loading, @@ -111,14 +100,6 @@ const PageHeaderComponent: FC & { {subtitle && {subtitle}} - - {leftActions} - - } - /> {actions}} diff --git a/frontend/src/component/project/ProjectList/ProjectGroup.tsx b/frontend/src/component/project/ProjectList/ProjectGroup.tsx new file mode 100644 index 000000000000..0e945241afff --- /dev/null +++ b/frontend/src/component/project/ProjectList/ProjectGroup.tsx @@ -0,0 +1,138 @@ +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 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'; + +const StyledProjectGroupContainer = styled('article')(({ theme }) => ({ + h3: { + marginBlockEnd: theme.spacing(2), + }, + + '&+&': { + marginBlockStart: theme.spacing(4), + }, +})); + +/** + * @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', + gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', + gap: theme.spacing(2), +})); + +const StyledCardLink = styled(Link)(({ theme }) => ({ + color: 'inherit', + textDecoration: 'none', + border: 'none', + padding: '0', + background: 'transparent', + fontFamily: theme.typography.fontFamily, + pointer: 'cursor', +})); + +export const ProjectGroup: React.FC<{ + sectionTitle?: string; + projects: IProjectCard[]; + loading: boolean; + searchValue: string; + handleHover: (id: string) => void; +}> = ({ sectionTitle, projects, loading, searchValue, handleHover }) => { + const useNewProjectCards = useUiFlag('projectsListNewCards'); + + const [StyledItemsContainer, ProjectCard] = useNewProjectCards + ? [StyledGridContainer, NewProjectCard] + : [StyledDivContainer, LegacyProjectCard]; + + return ( + + {sectionTitle}} + /> + 0} + show={ + + No projects found matching “ + {searchValue} + ” + + } + elseShow={ + + No projects available. + + } + /> + } + elseShow={ + + + loadingData.map((project: IProjectCard) => ( + {}} + key={project.id} + name={project.name} + id={project.id} + mode={project.mode} + memberCount={2} + health={95} + featureCount={4} + /> + )) + } + elseShow={() => ( + <> + {projects.map((project: IProjectCard) => ( + + + handleHover(project.id) + } + name={project.name} + mode={project.mode} + memberCount={ + project.memberCount ?? 0 + } + health={project.health} + id={project.id} + featureCount={ + project.featureCount + } + isFavorite={project.favorite} + /> + + ))} + + )} + /> + + } + /> + + ); +}; diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index 6dada6d26e3a..ce4283a5bed1 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -1,13 +1,10 @@ import { useContext, useEffect, useMemo, useState } from 'react'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { mutate } from 'swr'; import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { ProjectCard as LegacyProjectCard } from '../ProjectCard/ProjectCard'; -import { ProjectCard as NewProjectCard } from '../NewProjectCard/NewProjectCard'; import type { IProjectCard } from 'interfaces/project'; -import loadingData from './loadingData'; import { PageContent } from 'component/common/PageContent/PageContent'; import AccessContext from 'contexts/AccessContext'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -16,13 +13,7 @@ import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import Add from '@mui/icons-material/Add'; import ApiError from 'component/common/ApiError/ApiError'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { TablePlaceholder } from 'component/common/Table'; -import { - useMediaQuery, - styled, - ToggleButtonGroup, - ToggleButton, -} from '@mui/material'; +import { useMediaQuery, styled } from '@mui/material'; import theme from 'themes/theme'; import { Search } from 'component/common/Search/Search'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; @@ -33,58 +24,14 @@ import { safeRegExp } from '@server/util/escape-regex'; import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; import { useUiFlag } from 'hooks/useUiFlag'; import { useProfile } from 'hooks/api/getters/useProfile/useProfile'; -import { shouldDisplayInMyProjects } from './should-display-in-my-projects'; - -/** - * @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', - gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', - gap: theme.spacing(2), -})); +import { groupProjects } from './group-projects'; +import { ProjectGroup } from './ProjectGroup'; const StyledApiError = styled(ApiError)(({ theme }) => ({ maxWidth: '400px', marginBottom: theme.spacing(2), })); -const StyledCardLink = styled(Link)(({ theme }) => ({ - color: 'inherit', - textDecoration: 'none', - border: 'none', - padding: '0', - background: 'transparent', - fontFamily: theme.typography.fontFamily, - pointer: 'cursor', -})); - -const StyledButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({ - button: { - color: theme.palette.primary.main, - borderColor: theme.palette.background.alternative, - textTransform: 'none', - paddingInline: theme.spacing(3), - transition: 'background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - }, - 'button[aria-pressed=true]': { - backgroundColor: theme.palette.background.alternative, - color: theme.palette.primary.contrastText, - - '&:hover': { - backgroundColor: theme.palette.action.alternative, - }, - }, -})); - type PageQueryType = Partial>; type projectMap = { @@ -148,10 +95,7 @@ export const ProjectListNew = () => { searchParams.get('search') || '', ); - const showProjectFilterButtons = useUiFlag('projectListFilterMyProjects'); - const projectsListNewCards = useUiFlag('projectsListNewCards'); - const filters = ['All projects', 'My projects']; - const [filter, setFilter] = useState(filters[0]); + const splitProjectList = useUiFlag('projectListFilterMyProjects'); const myProjects = new Set(useProfile().profile?.projects || []); useEffect(() => { @@ -166,18 +110,11 @@ export const ProjectListNew = () => { }, [searchValue, setSearchParams]); const filteredProjects = useMemo(() => { - const preFilteredProjects = - showProjectFilterButtons && filter === 'My projects' - ? projects.filter(shouldDisplayInMyProjects(myProjects)) - : projects; - const regExp = safeRegExp(searchValue, 'i'); return ( searchValue - ? preFilteredProjects.filter((project) => - regExp.test(project.name), - ) - : preFilteredProjects + ? projects.filter((project) => regExp.test(project.name)) + : projects ).sort((a, b) => { if (a?.favorite && !b?.favorite) { return -1; @@ -187,7 +124,14 @@ export const ProjectListNew = () => { } return 0; }); - }, [projects, searchValue, filter, myProjects, showProjectFilterButtons]); + }, [projects, searchValue]); + + const groupedProjects = useMemo(() => { + if (!splitProjectList) { + return { myProjects: [], otherProjects: filteredProjects }; + } + return groupProjects(myProjects, filteredProjects); + }, [filteredProjects, myProjects, splitProjectList]); const handleHover = (projectId: string) => { if (fetchedProjects[projectId]) { @@ -215,49 +159,25 @@ export const ProjectListNew = () => { ? `${filteredProjects.length} of ${projects.length}` : projects.length; - const StyledItemsContainer = projectsListNewCards - ? StyledGridContainer - : StyledDivContainer; - const ProjectCard = projectsListNewCards - ? NewProjectCard - : LegacyProjectCard; - + const ProjectGroupComponent = (props: { + sectionTitle?: string; + projects: IProjectCard[]; + }) => { + return ( + + ); + }; return ( { - if (value !== null) { - setFilter(value); - } - }} - > - {filters.map((filter) => { - return ( - - {filter} - - ); - })} - - } - /> - } actions={ <> { } > - - 0} - show={ - - No projects found matching “ - {searchValue} - ” - - } - elseShow={ - - No projects available. - - } + + - } - elseShow={ - - loadingData.map((project: IProjectCard) => ( - {}} - key={project.id} - name={project.name} - id={project.id} - mode={project.mode} - memberCount={2} - health={95} - featureCount={4} - /> - )) - } - elseShow={() => - filteredProjects.map( - (project: IProjectCard) => ( - - - handleHover(project.id) - } - name={project.name} - mode={project.mode} - memberCount={ - project.memberCount ?? 0 - } - health={project.health} - id={project.id} - featureCount={ - project.featureCount - } - isFavorite={project.favorite} - /> - - ), - ) - } + + - } - /> - + + } + elseShow={} + /> ); }; diff --git a/frontend/src/component/project/ProjectList/should-display-in-my-projects.test.ts b/frontend/src/component/project/ProjectList/group-projects.test.ts similarity index 70% rename from frontend/src/component/project/ProjectList/should-display-in-my-projects.test.ts rename to frontend/src/component/project/ProjectList/group-projects.test.ts index 7f2ed351c0ed..bf0f1198ed34 100644 --- a/frontend/src/component/project/ProjectList/should-display-in-my-projects.test.ts +++ b/frontend/src/component/project/ProjectList/group-projects.test.ts @@ -1,8 +1,8 @@ import type { IProjectCard } from 'interfaces/project'; -import { shouldDisplayInMyProjects } from './should-display-in-my-projects'; +import { groupProjects } from './group-projects'; test('should check that the project is a user project OR that it is a favorite', () => { - const myProjects = new Set(['my1', 'my2', 'my3']); + const myProjectIds = new Set(['my1', 'my2', 'my3']); const projects: IProjectCard[] = [ { id: 'my1', favorite: true }, @@ -23,12 +23,16 @@ test('should check that the project is a user project OR that it is a favorite', favorite, })); - const filtered = projects.filter(shouldDisplayInMyProjects(myProjects)); + const { myProjects, otherProjects } = groupProjects(myProjectIds, projects); - expect(filtered).toMatchObject([ + expect(myProjects).toMatchObject([ { id: 'my1' }, { id: 'my2' }, { id: 'my3' }, { id: 'fave-but-not-mine' }, ]); + expect(otherProjects).toMatchObject([ + { id: 'not-mine-not-fave' }, + { id: 'not-mine-undefined-fave' }, + ]); }); diff --git a/frontend/src/component/project/ProjectList/group-projects.ts b/frontend/src/component/project/ProjectList/group-projects.ts new file mode 100644 index 000000000000..1bef0635e6b9 --- /dev/null +++ b/frontend/src/component/project/ProjectList/group-projects.ts @@ -0,0 +1,18 @@ +import type { IProjectCard } from 'interfaces/project'; + +export const groupProjects = ( + myProjectIds: Set, + filteredProjects: IProjectCard[], +) => { + const mine: IProjectCard[] = []; + const other: IProjectCard[] = []; + + for (const project of filteredProjects) { + if (project.favorite || myProjectIds.has(project.id)) { + mine.push(project); + } else { + other.push(project); + } + } + return { myProjects: mine, otherProjects: other }; +}; diff --git a/frontend/src/component/project/ProjectList/should-display-in-my-projects.ts b/frontend/src/component/project/ProjectList/should-display-in-my-projects.ts deleted file mode 100644 index 742eb624e4d1..000000000000 --- a/frontend/src/component/project/ProjectList/should-display-in-my-projects.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { IProjectCard } from 'interfaces/project'; - -export const shouldDisplayInMyProjects = - (myProjectIds: Set) => - (project: IProjectCard): boolean => - project.favorite || myProjectIds.has(project.id);