mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-22 01:16:07 +02:00
Projects archive UI (#7842)
Closes [issue/1-2666](https://linear.app/unleash/issue/1-2666/archived-projects-view)
This commit is contained in:
parent
3c45a4b2a9
commit
f2b7e0278d
@ -32,7 +32,7 @@ const BreadcrumbNav = () => {
|
|||||||
const { isAdmin } = useContext(AccessContext);
|
const { isAdmin } = useContext(AccessContext);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const paths = location.pathname
|
let paths = location.pathname
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter((item) => item)
|
.filter((item) => item)
|
||||||
.filter(
|
.filter(
|
||||||
@ -55,9 +55,15 @@ const BreadcrumbNav = () => {
|
|||||||
.map(decodeURI);
|
.map(decodeURI);
|
||||||
|
|
||||||
if (location.pathname === '/insights') {
|
if (location.pathname === '/insights') {
|
||||||
|
// Because of sticky header in Insights
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paths.length === 1 && paths[0] === 'projects-archive') {
|
||||||
|
// It's not possible to use `projects/archive`, because it's :projectId path
|
||||||
|
paths = ['projects', 'archive'];
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBreadcrumbContainer>
|
<StyledBreadcrumbContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import type { ComponentProps, FC } from 'react';
|
||||||
import { SvgIcon } from '@mui/material';
|
import { SvgIcon } from '@mui/material';
|
||||||
import { ReactComponent as Svg } from 'assets/icons/projectIconSmall.svg';
|
import { ReactComponent as Svg } from 'assets/icons/projectIconSmall.svg';
|
||||||
|
|
||||||
export const ProjectIcon = () => (
|
export const ProjectIcon: FC<ComponentProps<typeof SvgIcon>> = ({
|
||||||
<SvgIcon component={Svg} viewBox={'0 0 14 10'} />
|
...props
|
||||||
);
|
}) => <SvgIcon component={Svg} viewBox={'0 0 14 10'} {...props} />;
|
||||||
|
@ -98,6 +98,13 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Projects",
|
"title": "Projects",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"menu": {},
|
||||||
|
"path": "/projects-archive",
|
||||||
|
"title": "Projects archive",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"menu": {
|
"menu": {
|
||||||
|
@ -9,6 +9,7 @@ import { NewUser } from 'component/user/NewUser/NewUser';
|
|||||||
import ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
import ResetPassword from 'component/user/ResetPassword/ResetPassword';
|
||||||
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword';
|
||||||
import { ProjectListNew } from 'component/project/ProjectList/ProjectList';
|
import { ProjectListNew } from 'component/project/ProjectList/ProjectList';
|
||||||
|
import { ArchiveProjectList } from 'component/project/ProjectList/ArchiveProjectList';
|
||||||
import RedirectArchive from 'component/archive/RedirectArchive';
|
import RedirectArchive from 'component/archive/RedirectArchive';
|
||||||
import CreateEnvironment from 'component/environments/CreateEnvironment/CreateEnvironment';
|
import CreateEnvironment from 'component/environments/CreateEnvironment/CreateEnvironment';
|
||||||
import EditEnvironment from 'component/environments/EditEnvironment/EditEnvironment';
|
import EditEnvironment from 'component/environments/EditEnvironment/EditEnvironment';
|
||||||
@ -125,6 +126,13 @@ export const routes: IRoute[] = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true },
|
menu: { mobile: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/projects-archive',
|
||||||
|
title: 'Projects archive',
|
||||||
|
component: ArchiveProjectList,
|
||||||
|
type: 'protected',
|
||||||
|
menu: {},
|
||||||
|
},
|
||||||
|
|
||||||
// Features
|
// Features
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,8 @@ import Delete from '@mui/icons-material/Delete';
|
|||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
import { flexRow } from 'themes/themeStyles';
|
||||||
|
|
||||||
export const StyledProjectCard = styled(Card)(({ theme }) => ({
|
export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
|
||||||
|
({ theme, disabled = false }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@ -14,12 +15,16 @@ export const StyledProjectCard = styled(Card)(({ theme }) => ({
|
|||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
'&:hover': {
|
|
||||||
transition: 'background-color 0.2s ease-in-out',
|
transition: 'background-color 0.2s ease-in-out',
|
||||||
|
backgroundColor: disabled
|
||||||
|
? theme.palette.neutral.light
|
||||||
|
: theme.palette.background.default,
|
||||||
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.neutral.light,
|
backgroundColor: theme.palette.neutral.light,
|
||||||
},
|
},
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const StyledProjectCardBody = styled(Box)(({ theme }) => ({
|
export const StyledProjectCardBody = styled(Box)(({ theme }) => ({
|
||||||
padding: theme.spacing(1, 2, 2, 2),
|
padding: theme.spacing(1, 2, 2, 2),
|
||||||
@ -72,11 +77,13 @@ export const StyledDivInfo = styled('div')(({ theme }) => ({
|
|||||||
padding: theme.spacing(0, 1),
|
padding: theme.spacing(0, 1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledParagraphInfo = styled('p')(({ theme }) => ({
|
export const StyledParagraphInfo = styled('p')<{ disabled?: boolean }>(
|
||||||
color: theme.palette.primary.dark,
|
({ theme, disabled = false }) => ({
|
||||||
fontWeight: 'bold',
|
color: disabled ? 'inherit' : theme.palette.primary.dark,
|
||||||
|
fontWeight: disabled ? 'normal' : 'bold',
|
||||||
fontSize: theme.typography.body1.fontSize,
|
fontSize: theme.typography.body1.fontSize,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const StyledIconBox = styled(Box)(({ theme }) => ({
|
export const StyledIconBox = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -87,3 +94,8 @@ export const StyledIconBox = styled(Box)(({ theme }) => ({
|
|||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const StyledActions = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
@ -20,7 +20,7 @@ interface IProjectCardProps {
|
|||||||
name: string;
|
name: string;
|
||||||
featureCount: number;
|
featureCount: number;
|
||||||
health: number;
|
health: number;
|
||||||
memberCount: number;
|
memberCount?: number;
|
||||||
id: string;
|
id: string;
|
||||||
onHover: () => void;
|
onHover: () => void;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
@ -32,7 +32,7 @@ export const ProjectCard = ({
|
|||||||
name,
|
name,
|
||||||
featureCount,
|
featureCount,
|
||||||
health,
|
health,
|
||||||
memberCount,
|
memberCount = 0,
|
||||||
onHover,
|
onHover,
|
||||||
id,
|
id,
|
||||||
mode,
|
mode,
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import {
|
||||||
|
StyledProjectCard,
|
||||||
|
StyledDivHeader,
|
||||||
|
StyledBox,
|
||||||
|
StyledCardTitle,
|
||||||
|
StyledDivInfo,
|
||||||
|
StyledParagraphInfo,
|
||||||
|
StyledProjectCardBody,
|
||||||
|
StyledIconBox,
|
||||||
|
StyledActions,
|
||||||
|
} from './NewProjectCard.styles';
|
||||||
|
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
|
||||||
|
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
|
||||||
|
import { ProjectOwners } from './ProjectOwners/ProjectOwners';
|
||||||
|
import type { ProjectSchemaOwners } from 'openapi';
|
||||||
|
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
||||||
|
import { formatDateYMDHM } from 'utils/formatDate';
|
||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import TimeAgo from 'react-timeago';
|
||||||
|
import { Box, Link, Tooltip } from '@mui/material';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
CREATE_PROJECT,
|
||||||
|
DELETE_PROJECT,
|
||||||
|
} from 'component/providers/AccessProvider/permissions';
|
||||||
|
import Undo from '@mui/icons-material/Undo';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import Delete from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
interface IProjectArchiveCardProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt?: string;
|
||||||
|
archivedAt?: string;
|
||||||
|
featureCount: number;
|
||||||
|
onRevive: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
mode: string;
|
||||||
|
owners?: ProjectSchemaOwners;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectArchiveCard: FC<IProjectArchiveCardProps> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
archivedAt,
|
||||||
|
featureCount = 0,
|
||||||
|
onRevive,
|
||||||
|
onDelete,
|
||||||
|
mode,
|
||||||
|
owners,
|
||||||
|
}) => {
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
const Actions: FC<{
|
||||||
|
id: string;
|
||||||
|
}> = ({ id }) => (
|
||||||
|
<StyledActions>
|
||||||
|
<PermissionIconButton
|
||||||
|
onClick={onRevive}
|
||||||
|
projectId={id}
|
||||||
|
permission={CREATE_PROJECT}
|
||||||
|
tooltipProps={{ title: 'Restore project' }}
|
||||||
|
data-testid={`revive-feature-flag-button`}
|
||||||
|
>
|
||||||
|
<Undo />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={DELETE_PROJECT}
|
||||||
|
projectId={id}
|
||||||
|
tooltipProps={{ title: 'Permanently delete project' }}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</PermissionIconButton>
|
||||||
|
</StyledActions>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledProjectCard disabled>
|
||||||
|
<StyledProjectCardBody>
|
||||||
|
<StyledDivHeader>
|
||||||
|
<StyledIconBox>
|
||||||
|
<ProjectIcon color='action' />
|
||||||
|
</StyledIconBox>
|
||||||
|
<StyledBox data-loading>
|
||||||
|
<StyledCardTitle>{name}</StyledCardTitle>
|
||||||
|
</StyledBox>
|
||||||
|
<ProjectModeBadge mode={mode} />
|
||||||
|
</StyledDivHeader>
|
||||||
|
<StyledDivInfo>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to={`/archive?search=project%3A${encodeURI(id)}`}
|
||||||
|
>
|
||||||
|
<StyledParagraphInfo disabled data-loading>
|
||||||
|
{featureCount}
|
||||||
|
</StyledParagraphInfo>
|
||||||
|
<p data-loading>
|
||||||
|
archived {featureCount === 1 ? 'flag' : 'flags'}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(archivedAt)}
|
||||||
|
show={
|
||||||
|
<Tooltip
|
||||||
|
title={formatDateYMDHM(
|
||||||
|
parseISO(archivedAt as string),
|
||||||
|
locationSettings.locale,
|
||||||
|
)}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<StyledParagraphInfo disabled data-loading>
|
||||||
|
Archived
|
||||||
|
</StyledParagraphInfo>
|
||||||
|
<p data-loading>
|
||||||
|
<TimeAgo
|
||||||
|
date={
|
||||||
|
new Date(archivedAt as string)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledDivInfo>
|
||||||
|
</StyledProjectCardBody>
|
||||||
|
<ProjectCardFooter id={id} Actions={Actions} disabled>
|
||||||
|
<ProjectOwners owners={owners} />
|
||||||
|
</ProjectCardFooter>
|
||||||
|
</StyledProjectCard>
|
||||||
|
);
|
||||||
|
};
|
@ -10,26 +10,36 @@ interface IProjectCardFooterProps {
|
|||||||
id: string;
|
id: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
Actions?: FC<{ id: string; isFavorite?: boolean }>;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledFooter = styled(Box)(({ theme }) => ({
|
const StyledFooter = styled(Box)<{ disabled: boolean }>(
|
||||||
display: 'grid',
|
({ theme, disabled }) => ({
|
||||||
gridTemplateColumns: 'auto 1fr auto',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
background: disabled
|
||||||
padding: theme.spacing(1.5, 3, 2.5, 3),
|
? theme.palette.background.paper
|
||||||
background: theme.palette.envAccordion.expanded,
|
: theme.palette.envAccordion.expanded,
|
||||||
boxShadow: theme.boxShadows.accordionFooter,
|
boxShadow: theme.boxShadows.accordionFooter,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(1.5, 0, 2.5, 3),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledFavoriteIconButton = styled(FavoriteIconButton)(({ theme }) => ({
|
const StyledFavoriteIconButton = styled(FavoriteIconButton)(({ theme }) => ({
|
||||||
marginRight: theme.spacing(-1),
|
margin: theme.spacing(1, 2, 0, 0),
|
||||||
marginBottom: theme.spacing(-1),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
const FavoriteAction: FC<{ id: string; isFavorite?: boolean }> = ({
|
||||||
children,
|
|
||||||
id,
|
id,
|
||||||
isFavorite = false,
|
isFavorite,
|
||||||
}) => {
|
}) => {
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
const { favorite, unfavorite } = useFavoriteProjectsApi();
|
const { favorite, unfavorite } = useFavoriteProjectsApi();
|
||||||
@ -48,14 +58,27 @@ export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
|||||||
setToastApiError('Something went wrong, could not update favorite');
|
setToastApiError('Something went wrong, could not update favorite');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFooter>
|
|
||||||
{children}
|
|
||||||
<StyledFavoriteIconButton
|
<StyledFavoriteIconButton
|
||||||
onClick={onFavorite}
|
onClick={onFavorite}
|
||||||
isFavorite={isFavorite}
|
isFavorite={Boolean(isFavorite)}
|
||||||
size='medium'
|
size='medium'
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
isFavorite = false,
|
||||||
|
Actions = FavoriteAction,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledFooter disabled={disabled}>
|
||||||
|
<StyledContainer>{children}</StyledContainer>
|
||||||
|
<Actions id={id} isFavorite={isFavorite} />
|
||||||
</StyledFooter>
|
</StyledFooter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { VFC } from 'react';
|
import type { FC } from 'react';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
@ -8,7 +8,7 @@ interface IProjectModeBadgeProps {
|
|||||||
mode: 'private' | 'protected' | 'public' | string;
|
mode: 'private' | 'protected' | 'public' | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectModeBadge: VFC<IProjectModeBadgeProps> = ({ mode }) => {
|
export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => {
|
||||||
if (mode === 'private') {
|
if (mode === 'private') {
|
||||||
return (
|
return (
|
||||||
<HtmlTooltip
|
<HtmlTooltip
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import { type FC, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import useProjectsArchive from 'hooks/api/getters/useProjectsArchive/useProjectsArchive';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import ApiError from 'component/common/ApiError/ApiError';
|
||||||
|
import { styled, useMediaQuery } from '@mui/material';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { ProjectGroup } from './ProjectGroup';
|
||||||
|
import { ProjectArchiveCard } from '../NewProjectCard/ProjectArchiveCard';
|
||||||
|
|
||||||
|
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||||
|
maxWidth: '500px',
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(4),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
|
export const ArchiveProjectList: FC = () => {
|
||||||
|
const { projects, loading, error, refetch } = useProjectsArchive();
|
||||||
|
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [searchValue, setSearchValue] = useState(
|
||||||
|
searchParams.get('search') || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tableState: PageQueryType = {};
|
||||||
|
if (searchValue) {
|
||||||
|
tableState.search = searchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(tableState, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}, [searchValue, setSearchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
isLoading={loading}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title={`Projects archive (${projects.length || 0})`}
|
||||||
|
actions={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={error}
|
||||||
|
show={() => (
|
||||||
|
<StyledApiError
|
||||||
|
onClick={refetch}
|
||||||
|
text='Error fetching projects'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProjectGroup
|
||||||
|
loading={loading}
|
||||||
|
searchValue={searchValue}
|
||||||
|
projects={projects}
|
||||||
|
placeholder='No archived projects found'
|
||||||
|
ProjectCardComponent={ProjectArchiveCard}
|
||||||
|
link={false}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { ComponentType } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ProjectCard } from '../NewProjectCard/NewProjectCard';
|
import { ProjectCard } from '../NewProjectCard/NewProjectCard';
|
||||||
@ -23,12 +24,25 @@ const StyledCardLink = styled(Link)(({ theme }) => ({
|
|||||||
pointer: 'cursor',
|
pointer: 'cursor',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProjectGroup: React.FC<{
|
type ProjectGroupProps<T extends { id: string } = IProjectCard> = {
|
||||||
sectionTitle?: string;
|
sectionTitle?: string;
|
||||||
projects: IProjectCard[];
|
projects: T[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
}> = ({ sectionTitle, projects, loading, searchValue }) => {
|
placeholder?: string;
|
||||||
|
ProjectCardComponent?: ComponentType<T & any>;
|
||||||
|
link?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectGroup = <T extends { id: string }>({
|
||||||
|
sectionTitle,
|
||||||
|
projects,
|
||||||
|
loading,
|
||||||
|
searchValue,
|
||||||
|
placeholder = 'No projects available.',
|
||||||
|
ProjectCardComponent = ProjectCard,
|
||||||
|
link = true,
|
||||||
|
}: ProjectGroupProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -56,9 +70,7 @@ export const ProjectGroup: React.FC<{
|
|||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>{placeholder}</TablePlaceholder>
|
||||||
No projects available.
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -87,28 +99,24 @@ export const ProjectGroup: React.FC<{
|
|||||||
)}
|
)}
|
||||||
elseShow={() => (
|
elseShow={() => (
|
||||||
<>
|
<>
|
||||||
{projects.map((project: IProjectCard) => (
|
{projects.map((project: T) =>
|
||||||
|
link ? (
|
||||||
<StyledCardLink
|
<StyledCardLink
|
||||||
key={project.id}
|
key={project.id}
|
||||||
to={`/projects/${project.id}`}
|
to={`/projects/${project.id}`}
|
||||||
>
|
>
|
||||||
<ProjectCard
|
<ProjectCardComponent
|
||||||
onHover={() => {}}
|
onHover={() => {}}
|
||||||
name={project.name}
|
{...project}
|
||||||
mode={project.mode}
|
|
||||||
memberCount={
|
|
||||||
project.memberCount ?? 0
|
|
||||||
}
|
|
||||||
health={project.health}
|
|
||||||
id={project.id}
|
|
||||||
featureCount={
|
|
||||||
project.featureCount
|
|
||||||
}
|
|
||||||
isFavorite={project.favorite}
|
|
||||||
owners={project.owners}
|
|
||||||
/>
|
/>
|
||||||
</StyledCardLink>
|
</StyledCardLink>
|
||||||
))}
|
) : (
|
||||||
|
<ProjectCardComponent
|
||||||
|
onHover={() => {}}
|
||||||
|
{...project}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -11,7 +11,8 @@ 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 { styled, useMediaQuery } from '@mui/material';
|
import { Link, styled, useMediaQuery } from '@mui/material';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
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';
|
||||||
@ -24,6 +25,7 @@ import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
|
|||||||
import { groupProjects } from './group-projects';
|
import { groupProjects } from './group-projects';
|
||||||
import { ProjectGroup } from './ProjectGroup';
|
import { ProjectGroup } from './ProjectGroup';
|
||||||
import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog';
|
import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||||
maxWidth: '500px',
|
maxWidth: '500px',
|
||||||
@ -38,10 +40,6 @@ const StyledContainer = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
type PageQueryType = Partial<Record<'search', string>>;
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
type projectMap = {
|
|
||||||
[index: string]: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ICreateButtonData {
|
interface ICreateButtonData {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
tooltip?: Omit<ITooltipResolverProps, 'children'>;
|
tooltip?: Omit<ITooltipResolverProps, 'children'>;
|
||||||
@ -128,6 +126,7 @@ export const ProjectListNew = () => {
|
|||||||
const [searchValue, setSearchValue] = useState(
|
const [searchValue, setSearchValue] = useState(
|
||||||
searchParams.get('search') || '',
|
searchParams.get('search') || '',
|
||||||
);
|
);
|
||||||
|
const archiveProjectsEnabled = useUiFlag('archiveProjects');
|
||||||
|
|
||||||
const myProjects = new Set(useProfile().profile?.projects || []);
|
const myProjects = new Set(useProfile().profile?.projects || []);
|
||||||
|
|
||||||
@ -201,6 +200,21 @@ export const ProjectListNew = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(archiveProjectsEnabled)}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to='/projects-archive'
|
||||||
|
>
|
||||||
|
Archived projects
|
||||||
|
</Link>
|
||||||
|
<PageHeader.Divider />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<ProjectCreationButton />
|
<ProjectCreationButton />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import type { ProjectSchema } from 'openapi';
|
||||||
|
|
||||||
|
// FIXME: import tpye
|
||||||
|
interface IProjectArchiveCard {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
archivedAt: string;
|
||||||
|
description: string;
|
||||||
|
featureCount: number;
|
||||||
|
owners?: ProjectSchema['owners'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement data fetching
|
||||||
|
const useProjectsArchive = () => {
|
||||||
|
return {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'Archived something',
|
||||||
|
id: 'archi',
|
||||||
|
createdAt: new Date('2024-08-10 16:06').toISOString(),
|
||||||
|
archivedAt: new Date('2024-08-12 17:07').toISOString(),
|
||||||
|
owners: [{ ownerType: 'system' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Second example',
|
||||||
|
id: 'pid',
|
||||||
|
createdAt: new Date('2024-08-10 16:06').toISOString(),
|
||||||
|
archivedAt: new Date('2024-08-12 17:07').toISOString(),
|
||||||
|
owners: [{ ownerType: 'system' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: undefined as any,
|
||||||
|
loading: false,
|
||||||
|
refetch: () => {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProjectsArchive;
|
@ -29,6 +29,7 @@ const theme = {
|
|||||||
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
||||||
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
||||||
accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)',
|
accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)',
|
||||||
|
reverseFooter: 'inset 0px -2px 4px rgba(32, 32, 33, 0.05)',
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: 'Sen, Roboto, sans-serif',
|
fontFamily: 'Sen, Roboto, sans-serif',
|
||||||
|
@ -21,6 +21,7 @@ export const theme = {
|
|||||||
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
|
||||||
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
|
||||||
accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)',
|
accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)',
|
||||||
|
reverseFooter: 'inset 0px -2px 4px rgba(32, 32, 33, 0.05)',
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: 'Sen, Roboto, sans-serif',
|
fontFamily: 'Sen, Roboto, sans-serif',
|
||||||
|
@ -35,6 +35,7 @@ declare module '@mui/material/styles' {
|
|||||||
primaryHeader: string;
|
primaryHeader: string;
|
||||||
separator: string;
|
separator: string;
|
||||||
accordionFooter: string;
|
accordionFooter: string;
|
||||||
|
reverseFooter: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user