mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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