diff --git a/src/components/ExploreTopProjectsPage/ExploreTopProjectsPage.tsx b/src/components/ExploreTopProjectsPage/ExploreTopProjectsPage.tsx index 389e99766..08b327985 100644 --- a/src/components/ExploreTopProjectsPage/ExploreTopProjectsPage.tsx +++ b/src/components/ExploreTopProjectsPage/ExploreTopProjectsPage.tsx @@ -5,7 +5,7 @@ import { routes } from '../../hooks/router'; import { Page, PageContent } from '../Page'; import { PageSep } from '../PageSep'; import { ExplorePageLayout } from '../ExplorePageLayout/ExplorePageLayout'; -import { ProjectListContainer, ProjectListItem } from '../ProjectListItem/ProjectListItem'; +import { ProjectListContainer, ProjectListItem } from '../ProjectListItem'; import { trpc } from '../../utils/trpcClient'; import { tr } from './ExploreTopProjectsPage.i18n'; diff --git a/src/components/ExporeProjectsPage/ExporeProjectsPage.tsx b/src/components/ExporeProjectsPage/ExporeProjectsPage.tsx index 160041a75..d151d8dcd 100644 --- a/src/components/ExporeProjectsPage/ExporeProjectsPage.tsx +++ b/src/components/ExporeProjectsPage/ExporeProjectsPage.tsx @@ -5,7 +5,7 @@ import { routes } from '../../hooks/router'; import { Page, PageContent } from '../Page'; import { PageSep } from '../PageSep'; import { ExplorePageLayout } from '../ExplorePageLayout/ExplorePageLayout'; -import { ProjectListItem, ProjectListContainer } from '../ProjectListItem/ProjectListItem'; +import { ProjectListItem, ProjectListContainer } from '../ProjectListItem'; import { trpc } from '../../utils/trpcClient'; import { tr } from './ExporeProjectsPage.i18n'; diff --git a/src/components/ProjectListItem.tsx b/src/components/ProjectListItem.tsx new file mode 100644 index 000000000..4931da79e --- /dev/null +++ b/src/components/ProjectListItem.tsx @@ -0,0 +1,79 @@ +import { FC, ReactNode } from 'react'; +import Link from 'next/link'; +import { EyeIcon, StarFilledIcon, Text, nullable } from '@taskany/bricks'; + +import { ActivityByIdReturnType } from '../../trpc/inferredTypes'; + +import { Table, TableCell, TableRow } from './Table'; +import { UserGroup } from './UserGroup'; + +interface ProjectListItemProps { + href?: string; + children?: ReactNode; + title: string; + owner?: ActivityByIdReturnType; + participants?: ActivityByIdReturnType[]; + starred?: boolean; + watching?: boolean; +} + +export const ProjectListContainer: FC<{ children: ReactNode; offset?: number }> = ({ children, offset = 0 }) => ( + + {children} +
+); + +export const ProjectListItem: React.FC = ({ + href, + children, + title, + owner, + participants, + starred, + watching, +}) => { + const row = ( + + + + {title} + + {children} + + + + {nullable(owner, (o) => ( + + ))} + + + {nullable(participants, (p) => (p.length ? : null))} + + + {nullable(starred, () => ( + + ))} + + + + {nullable(watching, () => ( + + ))} + + + ); + + return href ? ( + + {row} + + ) : ( + row + ); +}; + +export const ProjectItemStandalone: React.FC = (props) => ( + + + +); diff --git a/src/components/ProjectListItem/ProjectListItem.tsx b/src/components/ProjectListItem/ProjectListItem.tsx deleted file mode 100644 index 593b35da7..000000000 --- a/src/components/ProjectListItem/ProjectListItem.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import React, { - Children, - FC, - MouseEvent, - MouseEventHandler, - ReactNode, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import styled from 'styled-components'; -import Link from 'next/link'; -import { Badge, Button, EyeIcon, StarFilledIcon, Text, nullable } from '@taskany/bricks'; -import { gapS } from '@taskany/colors'; - -import { ActivityByIdReturnType, GoalByIdReturnType, ProjectByIdReturnType } from '../../../trpc/inferredTypes'; -import { Table, TableCell, TableRow } from '../Table'; -import { UserGroup } from '../UserGroup'; -import { GoalListItem, GoalsListContainer } from '../GoalListItem'; -import { Collapsable, collapseOffset } from '../CollapsableItem'; - -import { tr } from './ProjectListItem.i18n'; - -const StyledGoalsButton = styled(Button)` - margin-left: ${gapS}; - cursor: pointer; -`; - -export const ProjectListContainer: FC<{ children: ReactNode; offset?: number }> = ({ children, offset = 0 }) => ( - - {children} -
-); - -interface ProjectListItemProps { - href?: string; - children?: ReactNode; - title: string; - owner?: ActivityByIdReturnType; - participants?: ActivityByIdReturnType[]; - starred?: boolean; - watching?: boolean; -} - -interface ProjectListItemCollapsableProps { - href: string; - project: NonNullable; - goals?: NonNullable[]; - children?: (id: string[], deep?: number) => ReactNode; - onCollapsedChange?: (value: boolean) => void; - onGoalsCollapsedChange?: (value: boolean) => void; - loading?: boolean; - onTagClick?: React.ComponentProps['onTagClick']; - onClickProvider?: (g: NonNullable) => MouseEventHandler; - selectedResolver?: (id: string) => boolean; - deep?: number; -} - -export const ProjectListItem: React.FC = ({ - href, - children, - title, - owner, - participants, - starred, - watching, -}) => { - const row = ( - - - - {title} - - {children} - - - - {nullable(owner, (o) => ( - - ))} - - - {nullable(participants, (p) => (p.length ? : null))} - - - {nullable(starred, () => ( - - ))} - - - - {nullable(watching, () => ( - - ))} - - - ); - - return href ? ( - - {row} - - ) : ( - row - ); -}; - -export const ProjectListItemCollapsable: React.FC = ({ - project, - onTagClick, - onClickProvider, - selectedResolver, - onCollapsedChange, - onGoalsCollapsedChange, - children, - loading = false, - goals, - deep = 0, -}) => { - const [collapsed, setIsCollapsed] = useState(true); - const [collapsedGoals, setIsCollapsedGoals] = useState(true); - - const offset = collapseOffset * (collapsed ? deep - 1 : deep); - - const childs = useMemo(() => project.children.map(({ id }) => id), [project]); - const content = collapsed ? null : children?.(childs, deep + 1); - - const onClickEnabled = children && childs.length; - - const onClick = useCallback(() => { - if (onClickEnabled) { - setIsCollapsed((value) => !value); - } - }, [onClickEnabled]); - - useEffect(() => { - onCollapsedChange?.(collapsed); - }, [collapsed, onCollapsedChange]); - - useEffect(() => { - onGoalsCollapsedChange?.(collapsedGoals); - }, [collapsedGoals, onGoalsCollapsedChange]); - - const onHeaderButtonClick = useCallback((e: MouseEvent) => { - e.stopPropagation(); - - setIsCollapsedGoals((value) => !value); - }, []); - - return ( - - - - - - } - content={content} - deep={deep} - > - {!collapsedGoals && ( - - {nullable(goals, (goals) => - goals.map((g) => ( - - )), - )} - - )} - - ); -}; - -export const ProjectItemStandalone: React.FC = (props) => ( - - - -); diff --git a/src/components/ProjectListItem/ProjectListItem.i18n/en.json b/src/components/ProjectListItemCollapsable/ProjectListItem.i18n/en.json similarity index 100% rename from src/components/ProjectListItem/ProjectListItem.i18n/en.json rename to src/components/ProjectListItemCollapsable/ProjectListItem.i18n/en.json diff --git a/src/components/ProjectListItem/ProjectListItem.i18n/index.ts b/src/components/ProjectListItemCollapsable/ProjectListItem.i18n/index.ts similarity index 100% rename from src/components/ProjectListItem/ProjectListItem.i18n/index.ts rename to src/components/ProjectListItemCollapsable/ProjectListItem.i18n/index.ts diff --git a/src/components/ProjectListItem/ProjectListItem.i18n/ru.json b/src/components/ProjectListItemCollapsable/ProjectListItem.i18n/ru.json similarity index 100% rename from src/components/ProjectListItem/ProjectListItem.i18n/ru.json rename to src/components/ProjectListItemCollapsable/ProjectListItem.i18n/ru.json diff --git a/src/components/ProjectListItemCollapsable/ProjectListItemCollapsable.tsx b/src/components/ProjectListItemCollapsable/ProjectListItemCollapsable.tsx new file mode 100644 index 000000000..7dbf974bf --- /dev/null +++ b/src/components/ProjectListItemCollapsable/ProjectListItemCollapsable.tsx @@ -0,0 +1,89 @@ +import React, { MouseEvent, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { Button } from '@taskany/bricks'; +import { gapS } from '@taskany/colors'; + +import { ProjectByIdReturnType } from '../../../trpc/inferredTypes'; +import { GoalsListContainer } from '../GoalListItem'; +import { Collapsable, collapseOffset } from '../CollapsableItem'; +import { ProjectListContainer, ProjectListItem } from '../ProjectListItem'; + +import { tr } from './ProjectListItem.i18n'; + +const StyledGoalsButton = styled(Button)` + margin-left: ${gapS}; + cursor: pointer; +`; + +interface ProjectListItemCollapsableProps { + href: string; + project: NonNullable; + goals?: ReactNode; + children?: ReactNode; + onCollapsedChange?: (value: boolean) => void; + onGoalsCollapsedChange?: (value: boolean) => void; + loading?: boolean; + deep?: number; +} + +export const ProjectListItemCollapsable: React.FC = ({ + project, + onCollapsedChange, + onGoalsCollapsedChange, + children, + loading = false, + goals, + deep = 0, +}) => { + const [collapsed, setIsCollapsed] = useState(true); + const [collapsedGoals, setIsCollapsedGoals] = useState(true); + + const offset = collapseOffset * (collapsed ? deep - 1 : deep); + const childs = useMemo(() => project.children.map(({ id }) => id), [project]); + + const onClickEnabled = childs.length; + + useEffect(() => { + onCollapsedChange?.(collapsed); + }, [collapsed, onCollapsedChange]); + + useEffect(() => { + onGoalsCollapsedChange?.(collapsedGoals); + }, [collapsedGoals, onGoalsCollapsedChange]); + + const onClick = useCallback(() => { + if (onClickEnabled) { + setIsCollapsed((value) => !value); + } + }, [onClickEnabled]); + + const onHeaderButtonClick = useCallback((e: MouseEvent) => { + e.stopPropagation(); + + setIsCollapsedGoals((value) => !value); + }, []); + + return ( + + + + + + } + content={children} + deep={deep} + > + {!collapsedGoals && {goals}} + + ); +}; diff --git a/src/components/ProjectPage/ProjectPage.tsx b/src/components/ProjectPage/ProjectPage.tsx index 32ada3226..442a8e433 100644 --- a/src/components/ProjectPage/ProjectPage.tsx +++ b/src/components/ProjectPage/ProjectPage.tsx @@ -19,8 +19,9 @@ import { PageTitle } from '../PageTitle'; import { createFilterKeys } from '../../utils/hotkeys'; import { Nullish } from '../../types/void'; import { trpc } from '../../utils/trpcClient'; -import { FilterById, GoalByIdReturnType } from '../../../trpc/inferredTypes'; -import { ProjectListItemCollapsable } from '../ProjectListItem/ProjectListItem'; +import { FilterById, GoalByIdReturnType, ProjectByIdReturnType } from '../../../trpc/inferredTypes'; +import { ProjectListItemCollapsable } from '../ProjectListItemCollapsable/ProjectListItemCollapsable'; +import { GoalListItem } from '../GoalListItem'; import { tr } from './ProjectPage.i18n'; @@ -30,18 +31,18 @@ const FilterCreateForm = dynamic(() => import('../FilterCreateForm/FilterCreateF const FilterDeleteForm = dynamic(() => import('../FilterDeleteForm/FilterDeleteForm')); const PageProjectListItem: FC< - Omit, 'children' | 'goals' | 'project'> & { - id: string; + Omit, 'children' | 'goals'> & { queryState: QueryState; + onTagClick?: React.ComponentProps['onTagClick']; + onClickProvider?: (g: NonNullable) => MouseEventHandler; + selectedResolver?: (id: string) => boolean; } -> = ({ queryState, id, ...props }) => { - const project = trpc.project.getById.useQuery(id); - +> = ({ queryState, project, onClickProvider, onTagClick, selectedResolver, deep = 0, ...props }) => { const [fetchGoalsEnabled, setFetchGoalsEnabled] = useState(false); const { data: projectDeepInfo } = trpc.project.getDeepInfo.useQuery( { - id, + id: project.id, ...queryState, }, { @@ -54,13 +55,27 @@ const PageProjectListItem: FC< const [fetchChildEnabled, setFetchChildEnabled] = useState(false); const childrenQueries = trpc.useQueries((t) => - (project.data?.children.map(({ id }) => id) || []).map((id) => + (project?.children.map(({ id }) => id) || []).map((id) => t.project.getById(id, { enabled: fetchChildEnabled, refetchOnWindowFocus: false }), ), ); - const loading = useMemo(() => childrenQueries.some(({ isLoading }) => isLoading), [childrenQueries]); - const goals = useMemo(() => projectDeepInfo?.goals.filter((g) => g.projectId === id), [projectDeepInfo, id]); + const loading = useMemo(() => childrenQueries.some(({ status }) => status === 'loading'), [childrenQueries]); + const childrenProjects = useMemo( + () => + childrenQueries.reduce((acum, { data }) => { + if (data) { + acum.push(data); + } + return acum; + }, [] as NonNullable[]), + [childrenQueries], + ); + + const goals = useMemo( + () => projectDeepInfo?.goals.filter((g) => g.projectId === project.id), + [projectDeepInfo, project], + ); const onCollapsedChange = useCallback((value: boolean) => { setFetchChildEnabled(!value); @@ -70,20 +85,42 @@ const PageProjectListItem: FC< setFetchGoalsEnabled(!value); }, []); - if (!project.data) return null; - return ( ( + )} + onTagClick={onTagClick} + /> + ))} + project={project} onCollapsedChange={onCollapsedChange} onGoalsCollapsedChange={onGoalsCollapsedChange} loading={loading} {...props} + deep={deep} > - {(ids, deep) => - ids.map((id) => ) - } + {childrenProjects.map((p) => ( + + ))} ); }; @@ -296,7 +333,7 @@ export const ProjectPage = ({ user, locale, ssrTime, params: { id } }: ExternalP ` +export const Table = styled.div<{ columns: number; minmax?: number; offset?: number }>` display: grid; grid-template-columns: ${({ columns, minmax = 410, offset = 0 }) => { if (columns < 2) { @@ -11,10 +11,10 @@ export const Table = styled.div<{ columns: number; minmax?: number, offset?: num } if (columns === 2) { - return `minmax(${minmax - offset}px, 30%) repeat(10, max-content) 1fr`; + return `${minmax - offset}px repeat(10, max-content) 1fr`; } - return `minmax(${minmax - offset}px, 30%) repeat(${columns - 2}, max-content) 1fr`; + return `${minmax - offset}px repeat(${columns - 2}, max-content) 1fr`; }}; width: 100%; diff --git a/trpc/queries/goals.ts b/trpc/queries/goals.ts index 506c6a24b..5c1394869 100644 --- a/trpc/queries/goals.ts +++ b/trpc/queries/goals.ts @@ -282,6 +282,31 @@ export const goalDeepQuery = { }, }, }, + goalAchiveCriteria: { + include: { + goalAsCriteria: { + include: { + estimate: { include: { estimate: true } }, + activity: { + include: { + user: true, + ghost: true, + }, + }, + owner: { + include: { + user: true, + ghost: true, + }, + }, + state: true, + }, + }, + }, + orderBy: { + createdAt: 'asc', + }, + }, dependsOn: { include: { state: true,