mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-12 13:48:35 +02:00
feat: split projects view into "my projects" and "other projects" (#6886)
This PR removes the previous "my projects" filter in favor always
splitting projects, but showing both on the main screen.
To make it a bit easier to work with, it also moves the project group
component into its own file, causing some extra lines of code change. My
apologies 🙇🏼
This commit is contained in:
parent
b8380a0b5b
commit
b6833d92aa
@ -57,15 +57,6 @@ const StyledHeaderActions = styled('div')(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
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 {
|
interface IPageHeaderProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
titleElement?: ReactNode;
|
titleElement?: ReactNode;
|
||||||
@ -73,7 +64,6 @@ interface IPageHeaderProps {
|
|||||||
variant?: TypographyProps['variant'];
|
variant?: TypographyProps['variant'];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
leftActions?: ReactNode;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
}
|
}
|
||||||
@ -84,7 +74,6 @@ const PageHeaderComponent: FC<IPageHeaderProps> & {
|
|||||||
title,
|
title,
|
||||||
titleElement,
|
titleElement,
|
||||||
actions,
|
actions,
|
||||||
leftActions,
|
|
||||||
subtitle,
|
subtitle,
|
||||||
variant,
|
variant,
|
||||||
loading,
|
loading,
|
||||||
@ -111,14 +100,6 @@ const PageHeaderComponent: FC<IPageHeaderProps> & {
|
|||||||
</StyledHeaderTitle>
|
</StyledHeaderTitle>
|
||||||
{subtitle && <small>{subtitle}</small>}
|
{subtitle && <small>{subtitle}</small>}
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(leftActions)}
|
|
||||||
show={
|
|
||||||
<StyledLeftHeaderActions>
|
|
||||||
{leftActions}
|
|
||||||
</StyledLeftHeaderActions>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(actions)}
|
condition={Boolean(actions)}
|
||||||
show={<StyledHeaderActions>{actions}</StyledHeaderActions>}
|
show={<StyledHeaderActions>{actions}</StyledHeaderActions>}
|
||||||
|
138
frontend/src/component/project/ProjectList/ProjectGroup.tsx
Normal file
138
frontend/src/component/project/ProjectList/ProjectGroup.tsx
Normal file
@ -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 (
|
||||||
|
<StyledProjectGroupContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(sectionTitle)}
|
||||||
|
show={<Typography component='h3'>{sectionTitle}</Typography>}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={projects.length < 1 && !loading}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={searchValue?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No projects found matching “
|
||||||
|
{searchValue}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No projects available.
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<StyledItemsContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={loading}
|
||||||
|
show={() =>
|
||||||
|
loadingData.map((project: IProjectCard) => (
|
||||||
|
<ProjectCard
|
||||||
|
data-loading
|
||||||
|
onHover={() => {}}
|
||||||
|
key={project.id}
|
||||||
|
name={project.name}
|
||||||
|
id={project.id}
|
||||||
|
mode={project.mode}
|
||||||
|
memberCount={2}
|
||||||
|
health={95}
|
||||||
|
featureCount={4}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
elseShow={() => (
|
||||||
|
<>
|
||||||
|
{projects.map((project: IProjectCard) => (
|
||||||
|
<StyledCardLink
|
||||||
|
key={project.id}
|
||||||
|
to={`/projects/${project.id}`}
|
||||||
|
>
|
||||||
|
<ProjectCard
|
||||||
|
onHover={() =>
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</StyledCardLink>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledItemsContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledProjectGroupContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -1,13 +1,10 @@
|
|||||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
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 { mutate } from 'swr';
|
||||||
import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher';
|
import { getProjectFetcher } from 'hooks/api/getters/useProject/getProjectFetcher';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 type { IProjectCard } from 'interfaces/project';
|
||||||
import loadingData from './loadingData';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
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 Add from '@mui/icons-material/Add';
|
||||||
import ApiError from 'component/common/ApiError/ApiError';
|
import ApiError from 'component/common/ApiError/ApiError';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { TablePlaceholder } from 'component/common/Table';
|
import { useMediaQuery, styled } from '@mui/material';
|
||||||
import {
|
|
||||||
useMediaQuery,
|
|
||||||
styled,
|
|
||||||
ToggleButtonGroup,
|
|
||||||
ToggleButton,
|
|
||||||
} from '@mui/material';
|
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
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 { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
|
import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
|
||||||
import { shouldDisplayInMyProjects } from './should-display-in-my-projects';
|
import { groupProjects } from './group-projects';
|
||||||
|
import { ProjectGroup } from './ProjectGroup';
|
||||||
/**
|
|
||||||
* @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 StyledApiError = styled(ApiError)(({ theme }) => ({
|
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||||
maxWidth: '400px',
|
maxWidth: '400px',
|
||||||
marginBottom: theme.spacing(2),
|
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<Record<'search', string>>;
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
type projectMap = {
|
type projectMap = {
|
||||||
@ -148,10 +95,7 @@ export const ProjectListNew = () => {
|
|||||||
searchParams.get('search') || '',
|
searchParams.get('search') || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
const showProjectFilterButtons = useUiFlag('projectListFilterMyProjects');
|
const splitProjectList = useUiFlag('projectListFilterMyProjects');
|
||||||
const projectsListNewCards = useUiFlag('projectsListNewCards');
|
|
||||||
const filters = ['All projects', 'My projects'];
|
|
||||||
const [filter, setFilter] = useState(filters[0]);
|
|
||||||
const myProjects = new Set(useProfile().profile?.projects || []);
|
const myProjects = new Set(useProfile().profile?.projects || []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -166,18 +110,11 @@ export const ProjectListNew = () => {
|
|||||||
}, [searchValue, setSearchParams]);
|
}, [searchValue, setSearchParams]);
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
const preFilteredProjects =
|
|
||||||
showProjectFilterButtons && filter === 'My projects'
|
|
||||||
? projects.filter(shouldDisplayInMyProjects(myProjects))
|
|
||||||
: projects;
|
|
||||||
|
|
||||||
const regExp = safeRegExp(searchValue, 'i');
|
const regExp = safeRegExp(searchValue, 'i');
|
||||||
return (
|
return (
|
||||||
searchValue
|
searchValue
|
||||||
? preFilteredProjects.filter((project) =>
|
? projects.filter((project) => regExp.test(project.name))
|
||||||
regExp.test(project.name),
|
: projects
|
||||||
)
|
|
||||||
: preFilteredProjects
|
|
||||||
).sort((a, b) => {
|
).sort((a, b) => {
|
||||||
if (a?.favorite && !b?.favorite) {
|
if (a?.favorite && !b?.favorite) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -187,7 +124,14 @@ export const ProjectListNew = () => {
|
|||||||
}
|
}
|
||||||
return 0;
|
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) => {
|
const handleHover = (projectId: string) => {
|
||||||
if (fetchedProjects[projectId]) {
|
if (fetchedProjects[projectId]) {
|
||||||
@ -215,49 +159,25 @@ export const ProjectListNew = () => {
|
|||||||
? `${filteredProjects.length} of ${projects.length}`
|
? `${filteredProjects.length} of ${projects.length}`
|
||||||
: projects.length;
|
: projects.length;
|
||||||
|
|
||||||
const StyledItemsContainer = projectsListNewCards
|
const ProjectGroupComponent = (props: {
|
||||||
? StyledGridContainer
|
sectionTitle?: string;
|
||||||
: StyledDivContainer;
|
projects: IProjectCard[];
|
||||||
const ProjectCard = projectsListNewCards
|
}) => {
|
||||||
? NewProjectCard
|
return (
|
||||||
: LegacyProjectCard;
|
<ProjectGroup
|
||||||
|
loading={loading}
|
||||||
|
searchValue={searchValue}
|
||||||
|
handleHover={handleHover}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={`Projects (${projectCount})`}
|
title={`Projects (${projectCount})`}
|
||||||
leftActions={
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={showProjectFilterButtons}
|
|
||||||
show={
|
|
||||||
<StyledButtonGroup
|
|
||||||
aria-label='project list filter'
|
|
||||||
size='small'
|
|
||||||
color='primary'
|
|
||||||
value={filter}
|
|
||||||
exclusive
|
|
||||||
onChange={(event, value) => {
|
|
||||||
if (value !== null) {
|
|
||||||
setFilter(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filters.map((filter) => {
|
|
||||||
return (
|
|
||||||
<ToggleButton
|
|
||||||
key={filter}
|
|
||||||
value={filter}
|
|
||||||
>
|
|
||||||
{filter}
|
|
||||||
</ToggleButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</StyledButtonGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -300,75 +220,23 @@ export const ProjectListNew = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ConditionallyRender condition={error} show={renderError()} />
|
<ConditionallyRender condition={error} show={renderError()} />
|
||||||
<StyledItemsContainer>
|
<ConditionallyRender
|
||||||
<ConditionallyRender
|
condition={splitProjectList}
|
||||||
condition={filteredProjects.length < 1 && !loading}
|
show={
|
||||||
show={
|
<>
|
||||||
<ConditionallyRender
|
<ProjectGroupComponent
|
||||||
condition={searchValue?.length > 0}
|
sectionTitle='My projects'
|
||||||
show={
|
projects={groupedProjects.myProjects}
|
||||||
<TablePlaceholder>
|
|
||||||
No projects found matching “
|
|
||||||
{searchValue}
|
|
||||||
”
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<TablePlaceholder>
|
|
||||||
No projects available.
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
elseShow={
|
<ProjectGroupComponent
|
||||||
<ConditionallyRender
|
sectionTitle='Other projects'
|
||||||
condition={loading}
|
projects={groupedProjects.otherProjects}
|
||||||
show={() =>
|
|
||||||
loadingData.map((project: IProjectCard) => (
|
|
||||||
<ProjectCard
|
|
||||||
data-loading
|
|
||||||
onHover={() => {}}
|
|
||||||
key={project.id}
|
|
||||||
name={project.name}
|
|
||||||
id={project.id}
|
|
||||||
mode={project.mode}
|
|
||||||
memberCount={2}
|
|
||||||
health={95}
|
|
||||||
featureCount={4}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
elseShow={() =>
|
|
||||||
filteredProjects.map(
|
|
||||||
(project: IProjectCard) => (
|
|
||||||
<StyledCardLink
|
|
||||||
key={project.id}
|
|
||||||
to={`/projects/${project.id}`}
|
|
||||||
>
|
|
||||||
<ProjectCard
|
|
||||||
onHover={() =>
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</StyledCardLink>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
</>
|
||||||
/>
|
}
|
||||||
</StyledItemsContainer>
|
elseShow={<ProjectGroupComponent projects={filteredProjects} />}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { IProjectCard } from 'interfaces/project';
|
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', () => {
|
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[] = [
|
const projects: IProjectCard[] = [
|
||||||
{ id: 'my1', favorite: true },
|
{ 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,
|
favorite,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filtered = projects.filter(shouldDisplayInMyProjects(myProjects));
|
const { myProjects, otherProjects } = groupProjects(myProjectIds, projects);
|
||||||
|
|
||||||
expect(filtered).toMatchObject([
|
expect(myProjects).toMatchObject([
|
||||||
{ id: 'my1' },
|
{ id: 'my1' },
|
||||||
{ id: 'my2' },
|
{ id: 'my2' },
|
||||||
{ id: 'my3' },
|
{ id: 'my3' },
|
||||||
{ id: 'fave-but-not-mine' },
|
{ id: 'fave-but-not-mine' },
|
||||||
]);
|
]);
|
||||||
|
expect(otherProjects).toMatchObject([
|
||||||
|
{ id: 'not-mine-not-fave' },
|
||||||
|
{ id: 'not-mine-undefined-fave' },
|
||||||
|
]);
|
||||||
});
|
});
|
18
frontend/src/component/project/ProjectList/group-projects.ts
Normal file
18
frontend/src/component/project/ProjectList/group-projects.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { IProjectCard } from 'interfaces/project';
|
||||||
|
|
||||||
|
export const groupProjects = (
|
||||||
|
myProjectIds: Set<string>,
|
||||||
|
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 };
|
||||||
|
};
|
@ -1,6 +0,0 @@
|
|||||||
import type { IProjectCard } from 'interfaces/project';
|
|
||||||
|
|
||||||
export const shouldDisplayInMyProjects =
|
|
||||||
(myProjectIds: Set<string>) =>
|
|
||||||
(project: IProjectCard): boolean =>
|
|
||||||
project.favorite || myProjectIds.has(project.id);
|
|
Loading…
Reference in New Issue
Block a user