From 0630b3fd99704f135a71d6b118e4c578e2ece28d Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 16 Jun 2023 21:40:53 +0300 Subject: [PATCH] feat(ProjectListItemCollapsible): add goals button --- src/components/CollapsableItem.tsx | 71 ++++++----- src/components/ProjectListItem.tsx | 130 +++++++++++++-------- src/components/ProjectPage/ProjectPage.tsx | 10 +- 3 files changed, 126 insertions(+), 85 deletions(-) diff --git a/src/components/CollapsableItem.tsx b/src/components/CollapsableItem.tsx index fc9811c99..4035d5411 100644 --- a/src/components/CollapsableItem.tsx +++ b/src/components/CollapsableItem.tsx @@ -1,6 +1,7 @@ import { FC, ReactNode } from 'react'; import styled, { css } from 'styled-components'; import { gray7, radiusM } from '@taskany/colors'; +import { nullable } from '@taskany/bricks'; export const collapseOffset = 20; @@ -34,11 +35,11 @@ const Dot = styled.div` const ParentDot = styled(Dot)``; -const CollapseHeader = styled.div` +const CollapsableHeader = styled.div` border-radius: ${radiusM}; `; -export const TableRowCollapseContent = styled.div` +export const CollapsableItem = styled.div` position: relative; &:before { @@ -48,12 +49,29 @@ export const TableRowCollapseContent = styled.div` } `; -const TableRowCollapseContainer = styled.div<{ collapsed: boolean; deep: number; showLine: boolean }>` +const CollapsableContainer = styled.div<{ collapsed: boolean; deep: number; showLine: boolean }>` position: relative; border-radius: ${radiusM}; - ${({ collapsed, deep, showLine }) => + > ${CollapsableItem}:before { + display: none; + } + + &:last-child:before { + display: none; + } + + &:last-child > ${CollapsableHeader}:after { + content: ''; + ${line} + + bottom: 50%; + + ${({ deep }) => deep === 0 && 'display: none;'} + } + + ${({ collapsed, deep }) => !collapsed && css` padding-left: ${collapseOffset}px; @@ -66,7 +84,7 @@ const TableRowCollapseContainer = styled.div<{ collapsed: boolean; deep: number; margin-left: -${collapseOffset}px; } - & > & > ${CollapseHeader}, & > ${CollapseHeader} { + & > & > ${CollapsableHeader}, & > ${CollapsableHeader} { padding-left: ${collapseOffset}px; margin-left: -${collapseOffset}px; position: relative; @@ -80,7 +98,7 @@ const TableRowCollapseContainer = styled.div<{ collapsed: boolean; deep: number; /** add parent dot if not first lvl */ - & > ${CollapseHeader} > ${ParentDot} { + & > ${CollapsableHeader} > ${ParentDot} { ${deep > 0 && css` display: block; @@ -91,18 +109,14 @@ const TableRowCollapseContainer = styled.div<{ collapsed: boolean; deep: number; /** first item vertical line */ & > &:before, - & > ${CollapseHeader}:before { + & > ${CollapsableHeader}:before { content: ''; ${line} } - & > ${TableRowCollapseContent}:before, & > ${CollapseHeader}:before { - ${!showLine && 'display: none;'} - } - - /** middle item vertical line */ + /** first item vertical line */ - & > ${CollapseHeader}:before { + & > ${CollapsableHeader}:before { top: 50%; } @@ -116,37 +130,36 @@ const TableRowCollapseContainer = styled.div<{ collapsed: boolean; deep: number; margin-left: -${collapseOffset}px; } - &:last-of-type:before { - display: none; - } - - &:last-of-type > ${CollapseHeader}:after { - content: ''; - ${line} + &:last-child > ${CollapsableHeader}:after { margin-left: -${collapseOffset}px; - bottom: 50%; + } - ${deep === 0 && 'display: none;'} + > ${CollapsableItem}:before { + display: block; } `} `; -export const TableRowCollapse: FC<{ +export const Collapsable: FC<{ children?: ReactNode; onClick?: () => void; header: ReactNode; + content: ReactNode; deep?: number; collapsed: boolean; showLine?: boolean; -}> = ({ onClick, children, header, collapsed, deep = 0, showLine = true }) => { +}> = ({ onClick, children, header, collapsed, deep = 0, showLine = true, content }) => { return ( - - + + {header} - - {!collapsed ? children : null} - + + {nullable(children, (ch) => ( + {ch} + ))} + {!collapsed ? content : null} + ); }; diff --git a/src/components/ProjectListItem.tsx b/src/components/ProjectListItem.tsx index 51626c73e..876f33ed1 100644 --- a/src/components/ProjectListItem.tsx +++ b/src/components/ProjectListItem.tsx @@ -1,24 +1,23 @@ -import React, { FC, MouseEventHandler, ReactNode, useCallback, useState } from 'react'; +import React, { FC, MouseEvent, MouseEventHandler, ReactNode, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import Link from 'next/link'; -import { EyeIcon, StarFilledIcon, Text, nullable } from '@taskany/bricks'; -import { gray6, radiusM } from '@taskany/colors'; +import { Badge, Button, EyeIcon, StarFilledIcon, Text, nullable } from '@taskany/bricks'; +import { gapS, gray3, gray4, gray6, radiusM } from '@taskany/colors'; import { ActivityByIdReturnType, GoalByIdReturnType, ProjectByIdReturnType } from '../../trpc/inferredTypes'; import { routes } from '../hooks/router'; +import { trpc } from '../utils/trpcClient'; +import { refreshInterval } from '../utils/config'; +import { QueryState } from '../hooks/useUrlFilterParams'; import { Table, TableCell, TableRow } from './Table'; import { UserGroup } from './UserGroup'; import { GoalListItem, GoalsListContainer } from './GoalListItem'; -import { TableRowCollapse, TableRowCollapseContent, collapseOffset } from './CollapsableItem'; +import { Collapsable, CollapsableItem, collapseOffset } from './CollapsableItem'; -const ProjectTableRowCollapseContent = styled(TableRowCollapseContent)` - // background: ${gray6}; - // border-radius: ${radiusM}; - - // ${TableRow}:hover ${TableCell} { - // background: ${gray6}; - // } +const ShowGoalsButton = styled(Button)` + margin-left: ${gapS}; + cursor: pointer; `; export const ProjectListContainer: FC<{ children: ReactNode; offset?: number }> = ({ children, offset = 0 }) => ( @@ -37,16 +36,17 @@ interface ProjectListItemBaseProps { interface ProjectListItemProps extends ProjectListItemBaseProps { href?: string; + children?: ReactNode; } interface ProjectListItemCollapsibleProps extends ProjectListItemBaseProps { id: string; href: string; - fetchGoals: (id: string) => Promise[] | null>; fetchProjects: (id: string) => Promise[] | null>; onTagClick?: React.ComponentProps['onTagClick']; onClickProvider?: (g: NonNullable) => MouseEventHandler; selectedResolver?: (id: string) => boolean; + queryState?: QueryState; deep?: number; } @@ -57,6 +57,7 @@ export const ProjectListItem: React.FC = ({ participants, starred, watching, + children, }) => { const row = ( @@ -64,6 +65,7 @@ export const ProjectListItem: React.FC = ({ {title} + {children} @@ -104,39 +106,62 @@ export const ProjectListItemCollapsible: React.FC { const [collapsed, setIsCollapsed] = useState(true); - const [goals, setGoals] = useState[]>(undefined); + const [collapsedGoals, setIsCollapsedGoals] = useState(true); const [projects, setProjects] = useState[]>(undefined); + const { data: projectDeepInfo } = trpc.project.getDeepInfo.useQuery( + { + id, + ...queryState, + }, + { + keepPreviousData: true, + staleTime: refreshInterval, + }, + ); + + const goals = useMemo( + () => (projectDeepInfo ? projectDeepInfo.goals.filter((g) => g.projectId === id) : null), + [projectDeepInfo, id], + ); + const offset = collapseOffset * (collapsed ? deep - 1 : deep); const onClick = useCallback(() => { - if (typeof goals === 'undefined' || typeof projects === 'undefined') { - Promise.all([fetchGoals(id), fetchProjects(id)]).then(([goals, projects]) => { - setGoals(goals); + if (typeof projects === 'undefined') { + fetchProjects(id).then((projects) => { setProjects(projects); - if (projects?.length || goals?.length) { + if (projects?.length) { setIsCollapsed(false); } }); - } else if (projects?.length || goals?.length) { + } else if (projects?.length) { setIsCollapsed((value) => !value); } - }, [fetchGoals, goals, projects]); + }, [fetchProjects, projects]); + + const onHeaderButtonClick = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + + setIsCollapsedGoals((value) => !value); + }, + [projects, collapsedGoals, onClick, collapsed], + ); return ( - + > + {goals?.length ?? 0}} + /> + } + content={nullable(projects, (projects) => + projects.map((p, i) => ( + + )), + )} deep={deep} > - {nullable(goals, (goals) => ( - + {!collapsedGoals && + nullable(goals, (goals) => ( {goals.map((g) => ( ))} - - ))} - - {nullable(projects, (projects) => - projects.map((p, i) => ( - - )), - )} - + ))} + ); }; diff --git a/src/components/ProjectPage/ProjectPage.tsx b/src/components/ProjectPage/ProjectPage.tsx index 9384e505b..30eaa0cd6 100644 --- a/src/components/ProjectPage/ProjectPage.tsx +++ b/src/components/ProjectPage/ProjectPage.tsx @@ -15,7 +15,6 @@ import { useWillUnmount } from '../../hooks/useWillUnmount'; import { routes } from '../../hooks/router'; import { ProjectPageLayout } from '../ProjectPageLayout/ProjectPageLayout'; import { Page, PageContent } from '../Page'; -import { GoalsGroup } from '../GoalsGroup'; import { PageTitle } from '../PageTitle'; import { createFilterKeys } from '../../utils/hotkeys'; import { Nullish } from '../../types/void'; @@ -212,12 +211,11 @@ export const ProjectPage = ({ user, locale, ssrTime, params: { id } }: ExternalP if (!project.data) return null; - const fetchGoals = useCallback((id: string) => Promise.resolve(projectMap[id].goals), [projectMap]); const fetchProjects = useCallback( (projectId: string): Promise[] | null> => { - if (projectId !== project.data?.id) { - return Promise.resolve(null); - } + // if (projectId !== project.data?.id) { + // return Promise.resolve(null); + // } return Promise.resolve( project.data?.children .map(({ id }) => { @@ -287,10 +285,10 @@ export const ProjectPage = ({ user, locale, ssrTime, params: { id } }: ExternalP starred={project.data?._isStarred} watching={project.data?._isWatching} fetchProjects={fetchProjects} - fetchGoals={fetchGoals} onTagClick={setTagsFilterOutside} onClickProvider={onGoalPrewiewShow} selectedResolver={selectedGoalResolver} + queryState={queryState} />