Skip to content

Commit

Permalink
feat(GrouppedGoals): rewrite prisma with kysely for get project list
Browse files Browse the repository at this point in the history
  • Loading branch information
LamaEats committed Aug 22, 2024
1 parent 5f023f7 commit 2aa9771
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/components/GoalsPage/GoalsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useUrlFilterParams } from '../../hooks/useUrlFilterParams';
import { useFiltersPreset } from '../../hooks/useFiltersPreset';
import { Page } from '../Page/Page';
import { getPageTitle } from '../../utils/getPageTitle';
import { GroupedGoalList } from '../GroupedGoalList';
import { GroupedGoalList } from '../GrouppedGoalListV2';
import { FlatGoalList } from '../FlatGoalList';
import { PresetModals } from '../PresetModals';
import { FiltersPanel } from '../FiltersPanel/FiltersPanel';
Expand Down
77 changes: 77 additions & 0 deletions src/components/GrouppedGoalListV2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useCallback, useEffect, useMemo } from 'react';
import { nullable } from '@taskany/bricks';
import { ListView } from '@taskany/bricks/harmony';

import { refreshInterval } from '../utils/config';
import { trpc } from '../utils/trpcClient';
import { useFMPMetric } from '../utils/telemetry';
import { FilterById, GoalByIdReturnType } from '../../trpc/inferredTypes';
import { useUrlFilterParams } from '../hooks/useUrlFilterParams';

import { LoadMoreButton } from './LoadMoreButton/LoadMoreButton';
import { useGoalPreview } from './GoalPreview/GoalPreviewProvider';
import { ProjectListItemConnected } from './ProjectListItemConnected';

interface GroupedGoalListProps {
filterPreset?: FilterById;
}

export const projectsSize = 20;

export const GroupedGoalList: React.FC<GroupedGoalListProps> = ({ filterPreset }) => {
const { setPreview, on } = useGoalPreview();

const { queryState } = useUrlFilterParams({
preset: filterPreset,
});

const utils = trpc.useContext();
const { data, fetchNextPage, hasNextPage } = trpc.v2.project.getAll.useInfiniteQuery(
{
limit: projectsSize,
goalsQuery: queryState,
firstLevel: !!queryState?.project?.length,
},
{
getNextPageParam: ({ pagination }) => pagination.offset,
staleTime: refreshInterval,
},
);

useEffect(() => {
const unsubUpdate = on('on:goal:update', () => utils.v2.project.getAll.invalidate());
const unsubDelete = on('on:goal:delete', () => utils.v2.project.getAll.invalidate());

return () => {
unsubUpdate();
unsubDelete();
};
}, [on, utils.v2.project.getAll]);

useFMPMetric(!!data);

const projectsOnScreen = useMemo(() => {
const pages = data?.pages || [];

return pages.flatMap((page) => page.projects);
}, [data]);

const handleItemEnter = useCallback(
(goal: NonNullable<GoalByIdReturnType>) => {
setPreview(goal._shortId, goal);
},
[setPreview],
);

return (
<ListView onKeyboardClick={handleItemEnter}>
{projectsOnScreen.map((project) => (
<ProjectListItemConnected key={project.id} project={project} filterPreset={filterPreset} />
))}

{nullable(hasNextPage, () => (
<LoadMoreButton onClick={() => fetchNextPage()} />
))}
</ListView>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { nullable } from '@taskany/bricks';
import { TreeView, TreeViewNode, Text, Link } from '@taskany/bricks/harmony';
import { IconServersOutline } from '@taskany/icons';

import { DashboardProject } from '../../../trpc/inferredTypes';
import { DashboardProjectV2 } from '../../../trpc/inferredTypes';
import { ProjectListItem } from '../ProjectListItem/ProjectListItem';
import { projectListItem, projectListItemTitle } from '../../utils/domObjects';
import { TableRowItem, TableRowItemTitle } from '../TableRowItem/TableRowItem';
Expand All @@ -13,8 +13,8 @@ import s from './ProjectListItemCollapsable.module.css';

interface ProjectListItemCollapsableProps extends Omit<ComponentProps<typeof TreeViewNode>, 'title'> {
href?: string;
project: NonNullable<DashboardProject>;
parent?: NonNullable<DashboardProject>;
project: NonNullable<DashboardProjectV2>;
parent?: NonNullable<DashboardProjectV2>;
goals?: ReactNode;
children?: React.ReactNode;
onClick?: MouseEventHandler<HTMLElement>;
Expand Down
12 changes: 6 additions & 6 deletions src/components/ProjectListItemConnected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,22 @@ export const ProjectListItemConnected: FC<ProjectListItemConnectedProps> = ({
},
);

const ids = useMemo(() => project?.children.map(({ id }) => id) || [], [project]);
const ids = useMemo(() => project?.children?.map(({ id }) => id) || [], [project]);
const { data: childrenProjects = [], isLoading } = trpc.project.getByIds.useQuery({ ids, goalsQuery: queryState });

useEffect(() => {
const unsubUpdate = on('on:goal:update', (updatedId) => {
const idInList = projectDeepInfo?.goals.find(({ _shortId }) => _shortId === updatedId);
if (idInList) {
utils.project.getDeepInfo.invalidate();
utils.project.getByIds.invalidate();
if (idInList?.projectId != null) {
utils.project.getDeepInfo.invalidate({ id: idInList.projectId });
utils.project.getByIds.invalidate({ ids: [idInList.projectId] });
}
});

const unsubDelete = on('on:goal:delete', (updatedId) => {
const idInList = projectDeepInfo?.goals.find(({ _shortId }) => _shortId === updatedId);
if (idInList) {
utils.project.getDeepInfo.invalidate();
if (idInList?.projectId != null) {
utils.project.getDeepInfo.invalidate({ id: idInList.projectId });
}
});

Expand Down
120 changes: 118 additions & 2 deletions trpc/queries/projectV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ export const getProjectsByIds = (params: { in: Array<{ id: string }>; activityId
activityId: ref('user.activityId'),
user: fn.toJson('user'),
}).as('activity'),
])
.select([
sql`(select count("B")::int from "_parentChildren" where "A" = "Project".id)`.as('children'),
sql`
case
Expand Down Expand Up @@ -623,3 +621,121 @@ export const getProjectSuggestions = ({
.orderBy(sql`CHAR_LENGTH(title)`)
.limit(limit);
};

interface GetAllProjectsQueryParams {
activityId: string;
role: Role;
firstLevel: boolean;
limit: number;
cursor: number;
ids?: string[];
}

export const getAllProjectsQuery = ({
activityId,
role,
firstLevel,
ids = [],
limit,
cursor,
}: GetAllProjectsQueryParams) => {
return db
.selectFrom(({ selectFrom }) =>
selectFrom('Project')
.leftJoinLateral(
({ selectFrom }) =>
getUserActivity()
.whereRef(
'Activity.id',
'in',
selectFrom('_projectParticipants').select('A').whereRef('B', '=', 'Project.id'),
)
.as('participants'),
(join) => join.onTrue(),
)
.leftJoinLateral(
({ selectFrom }) =>
selectFrom('Project as childrenProject')
.selectAll('childrenProject')
.where(
'childrenProject.id',
'in',
selectFrom('_parentChildren').select('B').whereRef('A', '=', 'Project.id'),
)
.as('ch'),
(join) => join.onTrue(),
)
.selectAll('Project')
.select(({ case: caseFn, fn }) => [
caseFn()
.when(fn.count('participants.id'), '>', 0)
.then(fn.agg('array_agg', [fn.toJson('participants')]))
.else(null)
.end()
.as('participants'),
caseFn()
.when(fn.count('ch.id'), '>', 0)
.then(fn.agg('array_agg', [fn.toJson('ch')]))
.else(null)
.end()
.as('children'),
jsonBuildObject({
stargizers: sql<number>`(select count("A") from "_projectStargizers" where "B" = "Project".id)`,
watchers: sql<number>`(select count("A") from "_projectWatchers" where "B" = "Project".id)`,
children: fn.count('ch.id'),
participants: sql<number>`(select count("A") from "_projectParticipants" where "B" = "Project".id)`,
goals: sql<number>`(select count("Goal".id) from "Goal" where "Goal"."projectId" = "Project".id)`,
}).as('_count'),
])
.where('Project.archived', 'is not', true)
.$if(ids && ids.length > 0, (qb) => qb.where('Project.id', 'in', ids))
.$if(role === Role.USER, (qb) =>
qb.where(({ or, eb, not, exists }) =>
or([
eb('Project.id', 'in', ({ selectFrom }) =>
selectFrom('_projectAccess').select('B').where('A', '=', activityId),
),
not(
exists(({ selectFrom }) =>
selectFrom('_projectAccess').select('B').whereRef('B', '=', 'Project.id'),
),
),
]),
),
)
.$if(firstLevel, (qb) =>
qb.where(({ not, exists, selectFrom }) =>
not(exists(selectFrom('_parentChildren').select('A').whereRef('B', '=', 'Project.id'))),
),
)
.limit(limit)
.offset(cursor)
.orderBy(['Project.updatedAt desc', 'Project.id desc'])
.groupBy(['Project.id'])
.as('projects'),
)
.innerJoinLateral(
() => getUserActivity().as('activity'),
(join) => join.onRef('activity.id', '=', 'projects.activityId'),
)
.selectAll('projects')
.select(({ fn, selectFrom, exists, val }) => [
fn.toJson('activity').as('activity'),
exists(
selectFrom('_projectWatchers')
.select('B')
.where('A', '=', activityId)
.whereRef('B', '=', 'projects.id'),
).as('_isWatching'),
exists(
selectFrom('_projectStargizers')
.select('B')
.where('A', '=', activityId)
.whereRef('B', '=', 'projects.id'),
).as('_isStarred'),
sql<boolean>`("projects"."activityId" = ${val(activityId)})`.as('_isOwner'),
sql<boolean>`((${val(role === Role.ADMIN)} or "projects"."activityId" = ${val(
activityId,
)}) and not "projects"."personal")`.as('_isEditable'),
]);
};
34 changes: 33 additions & 1 deletion trpc/router/projectV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getUserProjectsWithGoals,
getWholeGoalCountByProjectIds,
getDeepChildrenProjectsId,
getAllProjectsQuery,
} from '../queries/projectV2';
import { queryWithFiltersSchema } from '../../src/schema/common';
import {
Expand Down Expand Up @@ -41,7 +42,7 @@ type ProjectResponse = ExtractTypeFromGenerated<Project> & {
activity: ProjectActivity;
participants: ProjectActivity[];
goals?: any[]; // this prop is overrides below
children: any[]; // TODO: rly need this on Dashboard Page
children: ExtractTypeFromGenerated<Project>[] | null;
};

interface ProjectsWithGoals extends Pick<ProjectResponse, 'id'> {
Expand Down Expand Up @@ -224,4 +225,35 @@ export const project = router({
totalGoalsCount: goalsCountsByProjects?.wholeGoalsCount ?? 0,
};
}),

getAll: protectedProcedure
.input(
z.object({
limit: z.number().optional(),
goalsQuery: queryWithFiltersSchema.optional(),
firstLevel: z.boolean(),
cursor: z.number().optional(),
}),
)
.query(async ({ input, ctx }) => {
const { limit = 20, cursor = 0, goalsQuery, firstLevel: _ = true } = input;

const projects = await getAllProjectsQuery({
...ctx.session.user,
firstLevel: goalsQuery?.project == null,
limit: limit + 1,
cursor,
ids: goalsQuery?.project,
})
.$castTo<ProjectResponse & Pick<ProjectsWithGoals, '_count'>>()
.execute();

return {
projects: projects.slice(0, limit),
pagination: {
limit,
offset: projects.length < limit + 1 ? undefined : cursor + (limit ?? 0),
},
};
}),
});

0 comments on commit 2aa9771

Please sign in to comment.