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 }) => (
+
+);
+
+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 }) => (
-
-);
-
-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,