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 location = useLocation(); | ||||
| 
 | ||||
|     const paths = location.pathname | ||||
|     let paths = location.pathname | ||||
|         .split('/') | ||||
|         .filter((item) => item) | ||||
|         .filter( | ||||
| @ -55,9 +55,15 @@ const BreadcrumbNav = () => { | ||||
|         .map(decodeURI); | ||||
| 
 | ||||
|     if (location.pathname === '/insights') { | ||||
|         // Because of sticky header in Insights
 | ||||
|         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 ( | ||||
|         <StyledBreadcrumbContainer> | ||||
|             <ConditionallyRender | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import type { ComponentProps, FC } from 'react'; | ||||
| import { SvgIcon } from '@mui/material'; | ||||
| import { ReactComponent as Svg } from 'assets/icons/projectIconSmall.svg'; | ||||
| 
 | ||||
| export const ProjectIcon = () => ( | ||||
|     <SvgIcon component={Svg} viewBox={'0 0 14 10'} /> | ||||
| ); | ||||
| export const ProjectIcon: FC<ComponentProps<typeof SvgIcon>> = ({ | ||||
|     ...props | ||||
| }) => <SvgIcon component={Svg} viewBox={'0 0 14 10'} {...props} />; | ||||
|  | ||||
| @ -98,6 +98,13 @@ exports[`returns all baseRoutes 1`] = ` | ||||
|     "title": "Projects", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "menu": {}, | ||||
|     "path": "/projects-archive", | ||||
|     "title": "Projects archive", | ||||
|     "type": "protected", | ||||
|   }, | ||||
|   { | ||||
|     "component": [Function], | ||||
|     "menu": { | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { NewUser } from 'component/user/NewUser/NewUser'; | ||||
| import ResetPassword from 'component/user/ResetPassword/ResetPassword'; | ||||
| import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPassword'; | ||||
| import { ProjectListNew } from 'component/project/ProjectList/ProjectList'; | ||||
| import { ArchiveProjectList } from 'component/project/ProjectList/ArchiveProjectList'; | ||||
| import RedirectArchive from 'component/archive/RedirectArchive'; | ||||
| import CreateEnvironment from 'component/environments/CreateEnvironment/CreateEnvironment'; | ||||
| import EditEnvironment from 'component/environments/EditEnvironment/EditEnvironment'; | ||||
| @ -125,6 +126,13 @@ export const routes: IRoute[] = [ | ||||
|         type: 'protected', | ||||
|         menu: { mobile: true }, | ||||
|     }, | ||||
|     { | ||||
|         path: '/projects-archive', | ||||
|         title: 'Projects archive', | ||||
|         component: ArchiveProjectList, | ||||
|         type: 'protected', | ||||
|         menu: {}, | ||||
|     }, | ||||
| 
 | ||||
|     // Features
 | ||||
|     { | ||||
|  | ||||
| @ -4,7 +4,8 @@ import Delete from '@mui/icons-material/Delete'; | ||||
| import Edit from '@mui/icons-material/Edit'; | ||||
| import { flexRow } from 'themes/themeStyles'; | ||||
| 
 | ||||
| export const StyledProjectCard = styled(Card)(({ theme }) => ({ | ||||
| export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>( | ||||
|     ({ theme, disabled = false }) => ({ | ||||
|         display: 'flex', | ||||
|         flexDirection: 'column', | ||||
|         justifyContent: 'space-between', | ||||
| @ -14,12 +15,16 @@ export const StyledProjectCard = styled(Card)(({ theme }) => ({ | ||||
|         [theme.breakpoints.down('sm')]: { | ||||
|             justifyContent: 'center', | ||||
|         }, | ||||
|     '&:hover': { | ||||
|         transition: 'background-color 0.2s ease-in-out', | ||||
|         backgroundColor: disabled | ||||
|             ? theme.palette.neutral.light | ||||
|             : theme.palette.background.default, | ||||
|         '&:hover': { | ||||
|             backgroundColor: theme.palette.neutral.light, | ||||
|         }, | ||||
|         borderRadius: theme.shape.borderRadiusMedium, | ||||
| })); | ||||
|     }), | ||||
| ); | ||||
| 
 | ||||
| export const StyledProjectCardBody = styled(Box)(({ theme }) => ({ | ||||
|     padding: theme.spacing(1, 2, 2, 2), | ||||
| @ -72,11 +77,13 @@ export const StyledDivInfo = styled('div')(({ theme }) => ({ | ||||
|     padding: theme.spacing(0, 1), | ||||
| })); | ||||
| 
 | ||||
| export const StyledParagraphInfo = styled('p')(({ theme }) => ({ | ||||
|     color: theme.palette.primary.dark, | ||||
|     fontWeight: 'bold', | ||||
| export const StyledParagraphInfo = styled('p')<{ disabled?: boolean }>( | ||||
|     ({ theme, disabled = false }) => ({ | ||||
|         color: disabled ? 'inherit' : theme.palette.primary.dark, | ||||
|         fontWeight: disabled ? 'normal' : 'bold', | ||||
|         fontSize: theme.typography.body1.fontSize, | ||||
| })); | ||||
|     }), | ||||
| ); | ||||
| 
 | ||||
| export const StyledIconBox = styled(Box)(({ theme }) => ({ | ||||
|     display: 'grid', | ||||
| @ -87,3 +94,8 @@ export const StyledIconBox = styled(Box)(({ theme }) => ({ | ||||
|     color: theme.palette.primary.main, | ||||
|     height: '100%', | ||||
| })); | ||||
| 
 | ||||
| export const StyledActions = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     marginRight: theme.spacing(2), | ||||
| })); | ||||
|  | ||||
| @ -20,7 +20,7 @@ interface IProjectCardProps { | ||||
|     name: string; | ||||
|     featureCount: number; | ||||
|     health: number; | ||||
|     memberCount: number; | ||||
|     memberCount?: number; | ||||
|     id: string; | ||||
|     onHover: () => void; | ||||
|     isFavorite?: boolean; | ||||
| @ -32,7 +32,7 @@ export const ProjectCard = ({ | ||||
|     name, | ||||
|     featureCount, | ||||
|     health, | ||||
|     memberCount, | ||||
|     memberCount = 0, | ||||
|     onHover, | ||||
|     id, | ||||
|     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; | ||||
|     isFavorite?: boolean; | ||||
|     children?: React.ReactNode; | ||||
|     Actions?: FC<{ id: string; isFavorite?: boolean }>; | ||||
|     disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| const StyledFooter = styled(Box)(({ theme }) => ({ | ||||
|     display: 'grid', | ||||
|     gridTemplateColumns: 'auto 1fr auto', | ||||
|     alignItems: 'center', | ||||
|     padding: theme.spacing(1.5, 3, 2.5, 3), | ||||
|     background: theme.palette.envAccordion.expanded, | ||||
| const StyledFooter = styled(Box)<{ disabled: boolean }>( | ||||
|     ({ theme, disabled }) => ({ | ||||
|         display: 'flex', | ||||
|         background: disabled | ||||
|             ? theme.palette.background.paper | ||||
|             : theme.palette.envAccordion.expanded, | ||||
|         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 }) => ({ | ||||
|     marginRight: theme.spacing(-1), | ||||
|     marginBottom: theme.spacing(-1), | ||||
|     margin: theme.spacing(1, 2, 0, 0), | ||||
| })); | ||||
| 
 | ||||
| export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({ | ||||
|     children, | ||||
| const FavoriteAction: FC<{ id: string; isFavorite?: boolean }> = ({ | ||||
|     id, | ||||
|     isFavorite = false, | ||||
|     isFavorite, | ||||
| }) => { | ||||
|     const { setToastApiError } = useToast(); | ||||
|     const { favorite, unfavorite } = useFavoriteProjectsApi(); | ||||
| @ -48,14 +58,27 @@ export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({ | ||||
|             setToastApiError('Something went wrong, could not update favorite'); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledFooter> | ||||
|             {children} | ||||
|         <StyledFavoriteIconButton | ||||
|             onClick={onFavorite} | ||||
|                 isFavorite={isFavorite} | ||||
|             isFavorite={Boolean(isFavorite)} | ||||
|             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> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import type { VFC } from 'react'; | ||||
| import type { FC } from 'react'; | ||||
| import LockIcon from '@mui/icons-material/Lock'; | ||||
| import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; | ||||
| import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; | ||||
| @ -8,7 +8,7 @@ interface IProjectModeBadgeProps { | ||||
|     mode: 'private' | 'protected' | 'public' | string; | ||||
| } | ||||
| 
 | ||||
| export const ProjectModeBadge: VFC<IProjectModeBadgeProps> = ({ mode }) => { | ||||
| export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => { | ||||
|     if (mode === 'private') { | ||||
|         return ( | ||||
|             <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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { ProjectCard } from '../NewProjectCard/NewProjectCard'; | ||||
| @ -23,12 +24,25 @@ const StyledCardLink = styled(Link)(({ theme }) => ({ | ||||
|     pointer: 'cursor', | ||||
| })); | ||||
| 
 | ||||
| export const ProjectGroup: React.FC<{ | ||||
| type ProjectGroupProps<T extends { id: string } = IProjectCard> = { | ||||
|     sectionTitle?: string; | ||||
|     projects: IProjectCard[]; | ||||
|     projects: T[]; | ||||
|     loading: boolean; | ||||
|     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 ( | ||||
|         <article> | ||||
|             <ConditionallyRender | ||||
| @ -56,9 +70,7 @@ export const ProjectGroup: React.FC<{ | ||||
|                             </TablePlaceholder> | ||||
|                         } | ||||
|                         elseShow={ | ||||
|                             <TablePlaceholder> | ||||
|                                 No projects available. | ||||
|                             </TablePlaceholder> | ||||
|                             <TablePlaceholder>{placeholder}</TablePlaceholder> | ||||
|                         } | ||||
|                     /> | ||||
|                 } | ||||
| @ -87,28 +99,24 @@ export const ProjectGroup: React.FC<{ | ||||
|                             )} | ||||
|                             elseShow={() => ( | ||||
|                                 <> | ||||
|                                     {projects.map((project: IProjectCard) => ( | ||||
|                                     {projects.map((project: T) => | ||||
|                                         link ? ( | ||||
|                                             <StyledCardLink | ||||
|                                                 key={project.id} | ||||
|                                                 to={`/projects/${project.id}`} | ||||
|                                             > | ||||
|                                             <ProjectCard | ||||
|                                                 <ProjectCardComponent | ||||
|                                                     onHover={() => {}} | ||||
|                                                 name={project.name} | ||||
|                                                 mode={project.mode} | ||||
|                                                 memberCount={ | ||||
|                                                     project.memberCount ?? 0 | ||||
|                                                 } | ||||
|                                                 health={project.health} | ||||
|                                                 id={project.id} | ||||
|                                                 featureCount={ | ||||
|                                                     project.featureCount | ||||
|                                                 } | ||||
|                                                 isFavorite={project.favorite} | ||||
|                                                 owners={project.owners} | ||||
|                                                     {...project} | ||||
|                                                 /> | ||||
|                                             </StyledCardLink> | ||||
|                                     ))} | ||||
|                                         ) : ( | ||||
|                                             <ProjectCardComponent | ||||
|                                                 onHover={() => {}} | ||||
|                                                 {...project} | ||||
|                                             /> | ||||
|                                         ), | ||||
|                                     )} | ||||
|                                 </> | ||||
|                             )} | ||||
|                         /> | ||||
|  | ||||
| @ -11,7 +11,8 @@ import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions'; | ||||
| import Add from '@mui/icons-material/Add'; | ||||
| import ApiError from 'component/common/ApiError/ApiError'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { 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 { Search } from 'component/common/Search/Search'; | ||||
| 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 { ProjectGroup } from './ProjectGroup'; | ||||
| import { CreateProjectDialog } from '../Project/CreateProject/NewCreateProjectForm/CreateProjectDialog'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| 
 | ||||
| const StyledApiError = styled(ApiError)(({ theme }) => ({ | ||||
|     maxWidth: '500px', | ||||
| @ -38,10 +40,6 @@ const StyledContainer = styled('div')(({ theme }) => ({ | ||||
| 
 | ||||
| type PageQueryType = Partial<Record<'search', string>>; | ||||
| 
 | ||||
| type projectMap = { | ||||
|     [index: string]: boolean; | ||||
| }; | ||||
| 
 | ||||
| interface ICreateButtonData { | ||||
|     disabled: boolean; | ||||
|     tooltip?: Omit<ITooltipResolverProps, 'children'>; | ||||
| @ -128,6 +126,7 @@ export const ProjectListNew = () => { | ||||
|     const [searchValue, setSearchValue] = useState( | ||||
|         searchParams.get('search') || '', | ||||
|     ); | ||||
|     const archiveProjectsEnabled = useUiFlag('archiveProjects'); | ||||
| 
 | ||||
|     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 /> | ||||
|                         </> | ||||
|                     } | ||||
|  | ||||
| @ -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)', | ||||
|         separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
 | ||||
|         accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)', | ||||
|         reverseFooter: 'inset 0px -2px 4px rgba(32, 32, 33, 0.05)', | ||||
|     }, | ||||
|     typography: { | ||||
|         fontFamily: 'Sen, Roboto, sans-serif', | ||||
|  | ||||
| @ -21,6 +21,7 @@ export const theme = { | ||||
|         primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)', | ||||
|         separator: '0px 2px 4px rgba(32, 32, 33, 0.12)', // Notifications header
 | ||||
|         accordionFooter: 'inset 0px 2px 4px rgba(32, 32, 33, 0.05)', | ||||
|         reverseFooter: 'inset 0px -2px 4px rgba(32, 32, 33, 0.05)', | ||||
|     }, | ||||
|     typography: { | ||||
|         fontFamily: 'Sen, Roboto, sans-serif', | ||||
|  | ||||
| @ -35,6 +35,7 @@ declare module '@mui/material/styles' { | ||||
|             primaryHeader: string; | ||||
|             separator: string; | ||||
|             accordionFooter: string; | ||||
|             reverseFooter: string; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user