diff --git a/src/components/OfflineBanner/OfflineBanner.tsx b/src/components/OfflineBanner/OfflineBanner.tsx index f7afccf5d..53eb38f24 100644 --- a/src/components/OfflineBanner/OfflineBanner.tsx +++ b/src/components/OfflineBanner/OfflineBanner.tsx @@ -6,7 +6,7 @@ import { OfflineBanner as OfflineBannerBricks } from '@taskany/bricks/harmony'; import { tr } from './OfflineBanner.i18n'; export const OfflineBanner = () => { - const isOnline = useRef(null); + const isOnline = useRef(true); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, remoteServerStatus] = useOfflineDetector({ diff --git a/src/components/ProjectListItemConnected/ProjectListItemConnected.tsx b/src/components/ProjectListItemConnected/ProjectListItemConnected.tsx index 8c1c84141..bc119d656 100644 --- a/src/components/ProjectListItemConnected/ProjectListItemConnected.tsx +++ b/src/components/ProjectListItemConnected/ProjectListItemConnected.tsx @@ -2,17 +2,16 @@ import { FC, ComponentProps, useMemo, useState, useEffect } from 'react'; import { nullable } from '@taskany/bricks'; import { TreeViewElement } from '@taskany/bricks/harmony'; -import { FilterById } from '../../../trpc/inferredTypes'; +import { FilterById, ProjectChildrenTree } from '../../../trpc/inferredTypes'; import { useUrlFilterParams } from '../../hooks/useUrlFilterParams'; import { routes } from '../../hooks/router'; import { ProjectListItemCollapsable } from '../ProjectListItemCollapsable/ProjectListItemCollapsable'; import { ProjectGoalList } from '../ProjectGoalList/ProjectGoalList'; import { Kanban } from '../Kanban/Kanban'; -import { ProjectTree } from '../../../trpc/router/projectV2'; interface ProjectListItemConnectedProps extends ComponentProps { parent?: ComponentProps['project']; - subTree?: ProjectTree[string] | null; + subTree?: ProjectChildrenTree[string] | null; partnershipProject?: string[]; filterPreset?: FilterById; mainProject?: boolean; @@ -26,7 +25,7 @@ const onProjectClickHandler = (e: React.MouseEvent) => { } }; -const getIsProjectEmptySetter = (subTree?: ProjectTree[string] | null) => () => { +const getIsProjectEmptySetter = (subTree?: ProjectChildrenTree[string] | null) => () => { if (subTree) { if (Number(subTree.count) > 0) return false; diff --git a/src/components/ProjectPage/ProjectPage.tsx b/src/components/ProjectPage/ProjectPage.tsx index 0665cb721..4917e6199 100644 --- a/src/components/ProjectPage/ProjectPage.tsx +++ b/src/components/ProjectPage/ProjectPage.tsx @@ -1,11 +1,10 @@ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useDeferredValue, useEffect, useMemo } from 'react'; import NextLink from 'next/link'; import { nullable } from '@taskany/bricks'; import { ListView, Breadcrumb, Breadcrumbs, Link, FiltersBarItem } from '@taskany/bricks/harmony'; import { Page } from '../Page/Page'; -import { GoalByIdReturnType } from '../../../trpc/inferredTypes'; -import { refreshInterval } from '../../utils/config'; +import { GoalByIdReturnType, ProjectChildrenTree } from '../../../trpc/inferredTypes'; import { routes } from '../../hooks/router'; import { ExternalPageProps } from '../../utils/declareSsrProps'; import { useUrlFilterParams } from '../../hooks/useUrlFilterParams'; @@ -23,6 +22,29 @@ import s from './ProjectPage.module.css'; export const projectsSize = 20; +const countAvailableGoals = (subTree?: ProjectChildrenTree[string] | null) => { + let count = 0; + if (subTree) { + const nodes = [subTree.children]; + let i = 0; + let current = nodes[0]; + while (current) { + const keys = Object.keys(current); + for (const key of keys) { + count += current[key].count || 0; + + if (current[key].children) { + nodes.push(current[key].children); + } + } + i++; + current = nodes[i]; + } + } + + return count; +}; + export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallback }: ExternalPageProps) => { const utils = trpc.useContext(); @@ -32,16 +54,8 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba preset, }); - const [projectQuery, projectDeepInfoQuery, projectTreeQuery] = trpc.useQueries((ctx) => [ + const [projectQuery, projectTreeQuery] = trpc.useQueries((ctx) => [ ctx.v2.project.getById({ id }, { enabled: Boolean(id) }), - ctx.v2.project.getProjectGoalsById( - { id, goalsQuery: queryState }, - { - keepPreviousData: true, - staleTime: refreshInterval, - enabled: Boolean(id), - }, - ), ctx.v2.project.getProjectChildrenTree( { id, @@ -49,18 +63,20 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba }, { keepPreviousData: true, - staleTime: refreshInterval, + staleTime: Infinity, enabled: Boolean(id), }, ), ]); + const wholeGoalCountValue = useDeferredValue(countAvailableGoals(projectTreeQuery.data?.[id])); + const { setPreview, on } = useGoalPreview(); const invalidateFnsCallback = useCallback(() => { utils.v2.project.getById.invalidate(); - utils.v2.project.getProjectGoalsById.invalidate(); - }, [utils.v2.project.getProjectGoalsById, utils.v2.project.getById]); + utils.v2.project.getProjectChildrenTree.invalidate(); + }, [utils.v2.project.getProjectChildrenTree, utils.v2.project.getById]); useEffect(() => { const unsubUpdate = on('on:goal:update', invalidateFnsCallback); @@ -95,8 +111,8 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba header={ [number]; export type ProjectByIdReturnTypeV2 = RouterOutputs['v2']['project']['getById']; +export type ProjectChildrenTree = RouterOutputs['v2']['project']['getProjectChildrenTree']; export type GoalActivityHistory = RouterOutputs['goal']['getGoalActivityFeed']; export type GoalComments = RouterOutputs['goal']['getGoalCommentsFeed']; diff --git a/trpc/queries/projectV2.ts b/trpc/queries/projectV2.ts index 8653206d3..3da860049 100644 --- a/trpc/queries/projectV2.ts +++ b/trpc/queries/projectV2.ts @@ -802,13 +802,16 @@ export const getProjectChildrenTreeQuery = ({ id, goalsQuery }: { id: string; go .leftJoinLateral( ({ selectFrom }) => selectFrom('Goal') - .selectAll('Goal') + .select(({ fn }) => fn.count('Goal.id').as('goal_count')) .leftJoinLateral( - () => getUserActivity().as('participant'), - (join) => - join.onRef('participant.id', 'in', (qb) => - qb.selectFrom('_goalParticipants').select('A').whereRef('B', '=', 'Goal.id'), - ), + () => + getUserActivity() + .whereRef('Activity.id', 'in', (qb) => + // @ts-ignore + qb.selectFrom('_goalParticipants').select('A').whereRef('B', '=', 'Goal.id'), + ) + .as('participant'), + (join) => join.onTrue(), ) .leftJoinLateral( ({ selectFrom }) => @@ -823,16 +826,16 @@ export const getProjectChildrenTreeQuery = ({ id, goalsQuery }: { id: string; go .where('Goal.archived', 'is not', true) .where(getGoalsFiltersWhereExpressionBuilder(goalsQuery)) .as('goal'), - (join) => join.onRef('goal.projectId', '=', 'Project.id'), + (join) => join.onTrue(), ) - .select(({ fn, cast }) => [ + .select(({ cast, ref }) => [ 'Project.id', 'Project.title', - cast(fn.count('goal.id'), 'integer').as('goal_count'), - sql`to_json(ch.parent_chain)`.as('chain'), + cast(ref('goal.goal_count'), 'integer').as('goal_count'), + 'ch.parent_chain as chain', 'ch.level as deep', ]) - .groupBy(['Project.id', 'ch.level', 'ch.parent_chain']) + .groupBy(['Project.id', 'ch.level', 'ch.parent_chain', 'goal.goal_count']) .orderBy('ch.level asc'); };