diff --git a/frontend/src/component/project/ProjectCard/ProjectArchiveCard.tsx b/frontend/src/component/project/ProjectCard/ProjectArchiveCard.tsx index c7a6cae445..efce1f4bd3 100644 --- a/frontend/src/component/project/ProjectCard/ProjectArchiveCard.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectArchiveCard.tsx @@ -1,10 +1,11 @@ import type { FC } from 'react'; import { StyledProjectCard, - StyledBox, - StyledCardTitle, + StyledProjectCardTitle, StyledProjectCardBody, - StyledActions, + StyledProjectCardHeader, + StyledProjectCardContent, + StyledProjectCardTitleContainer, } from './ProjectCard.styles'; import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx'; import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx'; @@ -12,8 +13,8 @@ import type { ProjectSchemaOwners } from 'openapi'; import { formatDateYMDHM } from 'utils/formatDate'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { parseISO } from 'date-fns'; -import { Box, Link, styled, Tooltip } from '@mui/material'; -import { Link as RouterLink } from 'react-router-dom'; +import { Box, styled, Tooltip } from '@mui/material'; +import { Link } from 'react-router-dom'; import { DELETE_PROJECT, UPDATE_PROJECT, @@ -24,7 +25,12 @@ import Delete from '@mui/icons-material/Delete'; import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; -import { flexRow } from 'themes/themeStyles'; +import { Truncator } from 'component/common/Truncator/Truncator.tsx'; + +const StyledActions = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), +})); export type ProjectArchiveCardProps = { id: string; @@ -36,24 +42,6 @@ export type ProjectArchiveCardProps = { owners?: ProjectSchemaOwners; }; -export const StyledDivHeader = styled('div')(({ theme }) => ({ - ...flexRow, - width: '100%', - gap: theme.spacing(1), - minHeight: theme.spacing(6), - marginBottom: theme.spacing(1), -})); - -const StyledTitle = styled(StyledCardTitle)(({ theme }) => ({ - margin: 0, -})); - -const StyledContent = styled('div')(({ theme }) => ({ - ...flexRow, - fontSize: theme.fontSizes.smallerBody, - justifyContent: 'space-between', -})); - export const ProjectArchiveCard: FC = ({ id, name, @@ -69,51 +57,48 @@ export const ProjectArchiveCard: FC = ({ return ( - - - - - - {name} - - - - - - - - - ({ - color: theme.palette.text.secondary, - })} + + + -

- Archived:{' '} - -

-
-
- -

View archived flags

- -
+ + {name} + + + + + + + {archivedAt && ( +
+ Archived{' '} + + + + + +
+ )} +
+ + View archived flags + +
+
- + = ({ permission={UPDATE_PROJECT} tooltipProps={{ title: 'Revive project' }} data-testid={`revive-feature-flag-button`} + size='small' > @@ -129,6 +115,7 @@ export const ProjectArchiveCard: FC = ({ projectId={id} tooltipProps={{ title: 'Permanently delete project' }} onClick={onDelete} + size='small' > diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts b/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts index a4a2605198..cc432ccf50 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts +++ b/frontend/src/component/project/ProjectCard/ProjectCard.styles.ts @@ -1,8 +1,6 @@ import { styled } from '@mui/material'; import { Card, Box } from '@mui/material'; -import Delete from '@mui/icons-material/Delete'; -import Edit from '@mui/icons-material/Edit'; -import { flexRow } from 'themes/themeStyles'; +import { flexColumn, flexRow } from 'themes/themeStyles'; export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>( ({ theme, disabled = false }) => ({ @@ -16,11 +14,11 @@ export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>( justifyContent: 'center', }, transition: 'background-color 0.2s ease-in-out', - backgroundColor: disabled - ? theme.palette.neutral.light - : theme.palette.background.default, + backgroundColor: theme.palette.background.default, '&:hover': { - backgroundColor: theme.palette.neutral.light, + backgroundColor: disabled + ? theme.palette.background.default + : theme.palette.action.hover, }, borderRadius: theme.shape.borderRadiusMedium, }), @@ -33,63 +31,31 @@ export const StyledProjectCardBody = styled(Box)(({ theme }) => ({ justifyContent: 'space-between', height: '100%', position: 'relative', -})); - -export const StyledDivHeader = styled('div')(({ theme }) => ({ - ...flexRow, - width: '100%', - marginBottom: theme.spacing(2), gap: theme.spacing(1), })); -export const StyledCardTitle = styled('h3')<{ lines?: number }>( - ({ theme, lines = 2 }) => ({ - fontWeight: theme.typography.fontWeightRegular, - fontSize: theme.typography.body1.fontSize, - lineClamp: `${lines}`, - WebkitLineClamp: lines, - lineHeight: '1.2', - display: '-webkit-box', - boxOrient: 'vertical', - textOverflow: 'ellipsis', - overflow: 'hidden', - alignItems: 'flex-start', - WebkitBoxOrient: 'vertical', - wordBreak: 'break-word', - }), -); - -export const StyledBox = styled(Box)(() => ({ - ...flexRow, - marginRight: 'auto', -})); - -export const StyledEditIcon = styled(Edit)(({ theme }) => ({ - color: theme.palette.neutral.main, - marginRight: theme.spacing(1), -})); - -export const StyledDeleteIcon = styled(Delete)(({ theme }) => ({ - color: theme.palette.neutral.main, - marginRight: theme.spacing(1), -})); - -export const StyledDivInfo = styled('div')(({ theme }) => ({ +export const StyledProjectCardHeader = styled('div')(({ theme }) => ({ + gap: theme.spacing(1), display: 'flex', + width: '100%', + alignItems: 'center', +})); + +export const StyledProjectCardTitleContainer = styled('div')(({ theme }) => ({ + ...flexColumn, + margin: theme.spacing(1, 'auto', 1, 0), +})); + +export const StyledProjectCardTitle = styled('h3')(({ theme }) => ({ + margin: 0, + marginRight: 'auto', + fontWeight: theme.typography.fontWeightRegular, + fontSize: theme.typography.body1.fontSize, + lineHeight: '1.2', +})); + +export const StyledProjectCardContent = styled('div')(({ theme }) => ({ + ...flexRow, justifyContent: 'space-between', fontSize: theme.fontSizes.smallerBody, - padding: theme.spacing(0, 1), -})); - -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 StyledActions = styled(Box)(({ theme }) => ({ - display: 'flex', - margin: theme.spacing(0.5), })); diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.tsx b/frontend/src/component/project/ProjectCard/ProjectCard.tsx index 2145e62636..87dc35e358 100644 --- a/frontend/src/component/project/ProjectCard/ProjectCard.tsx +++ b/frontend/src/component/project/ProjectCard/ProjectCard.tsx @@ -1,13 +1,15 @@ import { + StyledProjectCardTitle, StyledProjectCard, - StyledCardTitle, StyledProjectCardBody, + StyledProjectCardHeader, + StyledProjectCardContent, + StyledProjectCardTitleContainer, } from './ProjectCard.styles'; import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx'; import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx'; import { FavoriteAction } from './FavoriteAction/FavoriteAction.tsx'; -import { Box, styled } from '@mui/material'; -import { flexColumn, flexRow } from 'themes/themeStyles'; +import { styled } from '@mui/material'; import { TimeAgo } from 'component/common/TimeAgo/TimeAgo'; import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen.tsx'; import { Highlighter } from 'component/common/Highlighter/Highlighter'; @@ -16,41 +18,18 @@ import { ProjectMembers } from './ProjectCardFooter/ProjectMembers/ProjectMember import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId'; import type { ProjectSchema } from 'openapi'; +import { Truncator } from 'component/common/Truncator/Truncator.tsx'; -const StyledUpdated = styled('span')(({ theme }) => ({ +const StyledSubtitle = styled('span')(({ theme }) => ({ color: theme.palette.text.secondary, fontSize: theme.fontSizes.smallerBody, })); -const StyledCount = styled('strong')(({ theme }) => ({ - fontWeight: theme.typography.fontWeightMedium, -})); - -const StyledInfo = styled('div')(({ theme }) => ({ - ...flexColumn, - fontSize: theme.fontSizes.smallerBody, -})); - -const StyledContent = styled('div')({ - ...flexRow, - justifyContent: 'space-between', -}); - -const StyledHeader = styled('div')(({ theme }) => ({ - gap: theme.spacing(1), - display: 'flex', - width: '100%', - alignItems: 'center', -})); - -type ProjectCardProps = ProjectSchema & { onHover?: () => void }; - export const ProjectCard = ({ name, featureCount, health, memberCount = 0, - onHover, id, mode, favorite = false, @@ -58,47 +37,47 @@ export const ProjectCard = ({ createdAt, lastUpdatedAt, lastReportedFlagUsage, -}: ProjectCardProps) => { +}: ProjectSchema) => { const { searchQuery } = useSearchHighlightContext(); return ( - + - - ({ - ...flexColumn, - margin: theme.spacing(1, 'auto', 1, 0), - })} - > - + + + {name} - - + + Updated{' '} - - + + - - -
- {featureCount} flag - {featureCount === 1 ? '' : 's'} -
- + +
+
- {health}% health + {featureCount} flag + {featureCount === 1 ? '' : 's'} +
+
+ +
+ {health}% health
- - +
+
( - ({ theme, disabled }) => ({ - display: 'flex', - background: disabled - ? theme.palette.background.paper - : theme.palette.background.elevation1, - boxShadow: theme.boxShadows.accordionFooter, - alignItems: 'center', - justifyContent: 'space-between', - borderTop: `1px solid ${theme.palette.divider}`, - paddingInline: theme.spacing(2), - paddingBlock: theme.spacing(1.5), - }), -); +const StyledFooter = styled(Box)(({ theme }) => ({ + display: 'flex', + background: theme.palette.background.elevation1, + boxShadow: theme.boxShadows.accordionFooter, + alignItems: 'center', + justifyContent: 'space-between', + borderTop: `1px solid ${theme.palette.divider}`, + paddingInline: theme.spacing(2), + paddingBlock: theme.spacing(1.5), +})); export const ProjectCardFooter: FC = ({ children, owners, - disabled = false, }) => { const ownersWithoutSystem = owners?.filter( (owner) => owner.ownerType !== 'system', ); return ( - + {ownersWithoutSystem ? ( { const ProjectCard = ProjectCardComponent ?? DefaultProjectCard; const { isOss } = useUiConfig(); - const { searchQuery } = useSearchHighlightContext(); return ( - <> + 0} - show={ - - No projects found matching “ - {searchQuery} - ” - - } - elseShow={ - {placeholder} - } - /> - } - elseShow={ - - ( - <> - {loadingData.map( - (project: ProjectSchema) => ( - - ), - )} - - )} - elseShow={() => ( - <> - {projects.map((project) => - link ? ( - - - - ) : ( - - ), - )} - - )} - /> - {isOss() ? : null} - - } + condition={loading} + show={() => ( + <> + {loadingData.map((project: ProjectSchema) => ( + + ))} + + )} + elseShow={() => ( + <> + {projects.map((project) => + link ? ( + + + + ) : ( + + ), + )} + + )} /> - + {isOss() ? : null} + ); }; diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index e0972e0088..7dbc97234d 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -18,6 +18,7 @@ import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort.ts'; import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx'; import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { TablePlaceholder } from 'component/common/Table/index.ts'; const StyledApiError = styled(ApiError)(({ theme }) => ({ maxWidth: '500px', @@ -38,7 +39,7 @@ export const ProjectList = () => { const [state, setState] = useProjectsListState(); - const myProjects = new Set(useProfile().profile?.projects || []); + const myProfileProjects = new Set(useProfile().profile?.projects || []); const setSearchValue = useCallback( (value: string) => setState({ query: value || undefined }), @@ -50,13 +51,20 @@ export const ProjectList = () => { state.query, state.sortBy, ); - const groupedProjects = useGroupedProjects(sortedProjects, myProjects); + const groupedProjects = useGroupedProjects( + sortedProjects, + myProfileProjects, + ); const projectCount = sortedProjects.length < projects.length ? `${sortedProjects.length} of ${projects.length}` : projects.length; + const myProjects = isOss() ? sortedProjects : groupedProjects.myProjects; + + const otherProjects = isOss() ? [] : groupedProjects.otherProjects; + return ( { actions={ <> { )} /> -
- - setState({ - sortBy: sortBy as typeof state.sortBy, - }) - } - /> - } - > - My projects - - -
- {!isOss() ? ( + {myProjects.length > 0 && (
- + + setState({ + sortBy: sortBy as typeof state.sortBy, + }) + } + /> + } + > + My projects + + +
+ )} + {otherProjects.length > 0 && ( +
+ Other projects
- ) : null} + )} + {!loading && + !myProjects.length && + !otherProjects.length && ( + <> + {state.query?.length ? ( + + No projects found matching “ + {state.query} + ” + + ) : ( + + No projects available. + + )} + + )}
diff --git a/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx b/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx index fdf630f479..2ed0c0f197 100644 --- a/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx +++ b/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx @@ -1,9 +1,10 @@ -import { styled, Typography } from '@mui/material'; +import { styled } from '@mui/material'; +import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; import type { FC, ReactNode } from 'react'; type ProjectsListHeaderProps = { - children?: ReactNode; - subtitle?: string; + children: ReactNode; + helpText: string; actions?: ReactNode; }; @@ -18,28 +19,22 @@ const StyledHeaderContainer = styled('div')(({ theme }) => ({ marginBottom: theme.spacing(2), })); -const StyledHeaderTitle = styled('div')(() => ({ +const StyledHeaderTitle = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), flexGrow: 0, })); export const ProjectsListHeader: FC = ({ children, - subtitle, + helpText, actions, }) => { return ( - {children ? ( - - {children} - - ) : null} - {subtitle ? ( - - {subtitle} - - ) : null} + {children} + {actions} diff --git a/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx b/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx index f3c9b11158..05fa4f1405 100644 --- a/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx +++ b/frontend/src/component/project/ProjectList/ProjectsListSort/ProjectsListSort.tsx @@ -9,7 +9,7 @@ const StyledWrapper = styled('div')(({ theme }) => ({ })); const StyledContainer = styled('div')(() => ({ - maxWidth: '220px', + maxWidth: '200px', width: '100%', }));