Skip to content

Commit

Permalink
fix: add recursive check on goal parent and children criteria
Browse files Browse the repository at this point in the history
  • Loading branch information
IgorGoryany committed Aug 26, 2024
1 parent 143f375 commit 1f405e6
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/components/GoalActivityFeed/GoalActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { nullable } from '@taskany/bricks';
import { forwardRef, useCallback } from 'react';
import dynamic from 'next/dynamic';

import { trpc } from '../../utils/trpcClient';
import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal';
import { editGoalKeys } from '../../utils/hotkeys';
import { GoalByIdReturnType } from '../../../trpc/inferredTypes';
Expand Down Expand Up @@ -147,6 +148,8 @@ export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps
[onGoalCriteriaConvert],
);

const { data: parentGoalIds = [] } = trpc.v2.goal.getParentIds.useQuery([goal.id]);

const criteriaValidityData = useCriteriaValidityData(goal._criteria);

return (
Expand Down Expand Up @@ -184,6 +187,7 @@ export const GoalActivityFeed = forwardRef<HTMLDivElement, GoalActivityFeedProps
onSubmit={handleCreateCriteria}
validateGoalCriteriaBindings={validateGoalCriteriaBindings}
validityData={criteriaValidityData}
filter={[goal.id, ...parentGoalIds.map((id) => id.id)]}
/>
</GoalFormPopupTrigger>
))}
Expand Down
6 changes: 6 additions & 0 deletions src/components/GoalCriteriaSuggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface GoalCriteriaSuggestProps {
items?: {
goal?: Goal | null;
}[];
filter?: string[];
versa?: boolean;
/** Value allows restrict search results by current user */
restrictedSearch?: boolean;
Expand All @@ -34,6 +35,7 @@ interface GoalCriteriaSuggestProps {
export const GoalCriteriaSuggest: React.FC<GoalCriteriaSuggestProps> = ({
id,
items,
filter,
withModeSwitch,
defaultMode = 'simple',
versa,
Expand Down Expand Up @@ -64,6 +66,7 @@ export const GoalCriteriaSuggest: React.FC<GoalCriteriaSuggestProps> = ({
input: query as string,
limit: 5,
onlyCurrentUser: restrictedSearch,
filter,
},
{ enabled: mode === 'goal', cacheTime: 0 },
);
Expand Down Expand Up @@ -182,6 +185,8 @@ export const VersaCriteriaSuggest: React.FC<VersaCriteriaSuggestProps> = ({ goal

const validityData = useCriteriaValidityData(data);

const { data: childrenIds = [] } = trpc.v2.goal.getChildrenIds.useQuery([goalId]);

return (
<GoalCriteriaSuggest
id={goalId}
Expand All @@ -191,6 +196,7 @@ export const VersaCriteriaSuggest: React.FC<VersaCriteriaSuggestProps> = ({ goal
validateGoalCriteriaBindings={validateGoalCriteriaBindings}
versa
restrictedSearch
filter={[goalId, ...childrenIds.map(({ id }) => id).filter(Boolean)]}
validityData={validityData}
onGoalSelect={({ id }) => setSelectedGoalId(id)}
/>
Expand Down
1 change: 1 addition & 0 deletions src/schema/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const suggestionsQuerySchema = z.object({
limit: z.number().optional(),
input: z.string(),
onlyCurrentUser: z.boolean().optional(),
filter: z.string().array().optional(),
});

export type SuggestionsQuerySchema = z.infer<typeof suggestionsQuerySchema>;
Expand Down
44 changes: 44 additions & 0 deletions trpc/queries/goalV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,47 @@ export const goalBaseQuery = () => {
sql`to_json("State")`.as('state'),
]);
};

export const getDeepParentGoalIds = (ids: string[]) => {
return db
.withRecursive('parentsTree', (qb) =>
qb
.selectFrom('GoalAchieveCriteria')
.select('GoalAchieveCriteria.goalId as id')
.where('GoalAchieveCriteria.criteriaGoalId', 'is not', null)
.where('GoalAchieveCriteria.criteriaGoalId', 'in', ids)
.where('GoalAchieveCriteria.deleted', 'is not', true)
.union((qb) =>
qb
.selectFrom('GoalAchieveCriteria')
.select('GoalAchieveCriteria.goalId as id')
.where('GoalAchieveCriteria.criteriaGoalId', 'is not', null)
.where('GoalAchieveCriteria.deleted', 'is not', true)
.innerJoin('parentsTree', 'parentsTree.id', 'GoalAchieveCriteria.criteriaGoalId'),
),
)
.selectFrom('parentsTree')
.selectAll();
};

export const getDeepChildrenGoalIds = (ids: string[]) => {
return db
.withRecursive('parentsTree', (qb) =>
qb
.selectFrom('GoalAchieveCriteria')
.select('GoalAchieveCriteria.criteriaGoalId as id')
.where('GoalAchieveCriteria.goalId', 'in', ids)
.where('GoalAchieveCriteria.criteriaGoalId', 'is not', null)
.where('GoalAchieveCriteria.deleted', 'is not', true)
.union((qb) =>
qb
.selectFrom('GoalAchieveCriteria')
.select('GoalAchieveCriteria.criteriaGoalId as id')
.where('GoalAchieveCriteria.criteriaGoalId', 'is not', null)
.where('GoalAchieveCriteria.deleted', 'is not', true)
.innerJoin('parentsTree', 'parentsTree.id', 'GoalAchieveCriteria.goalId'),
),
)
.selectFrom('parentsTree')
.selectAll();
};
14 changes: 13 additions & 1 deletion trpc/router/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ import { commentsByGoalIdQuery, reactionsForGoalComments } from '../queries/comm
import { ReactionsMap } from '../../src/types/reactions';
import { safeGetUserName } from '../../src/utils/getUserName';
import { extraDataForEachRecord, goalHistorySeparator, historyQuery } from '../queries/history';
import { getDeepParentGoalIds } from '../queries/goalV2';

import { tr } from './router.i18n';

export const goal = router({
suggestions: protectedProcedure
.input(suggestionsQuerySchema)
.query(async ({ ctx, input: { input, limit = 5, onlyCurrentUser = false } }) => {
.query(async ({ ctx, input: { input, limit = 5, onlyCurrentUser = false, filter } }) => {
const { activityId, role } = ctx.session.user || {};

const splittedInput = input.split('-');
Expand Down Expand Up @@ -118,6 +119,7 @@ export const goal = router({
project: {
...getProjectAccessFilter(activityId, role),
},
id: { not: { in: filter } },
},
},
include: getGoalDeepQuery({
Expand Down Expand Up @@ -1101,6 +1103,16 @@ export const goal = router({
}
}

const parentIds = await getDeepParentGoalIds([input.goalId]).execute();

if (
input.criteriaGoal?.id &&
(parentIds?.map(({ id }) => id).includes(input.criteriaGoal.id) ||
input.goalId === input.criteriaGoal.id)
) {
throw new TRPCError({ code: 'BAD_REQUEST', message: tr("Goal cannot be it's own criteria") });
}

try {
const newCriteria = await prisma.goalAchieveCriteria.create({
data: {
Expand Down
14 changes: 14 additions & 0 deletions trpc/router/goalV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod';

import { protectedProcedure, router } from '../trpcBackend';
import { getDeepParentGoalIds, getDeepChildrenGoalIds } from '../queries/goalV2';

export const goal = router({
getParentIds: protectedProcedure.input(z.string().array()).query(async ({ input }) => {
return getDeepParentGoalIds(input).execute();
}),

getChildrenIds: protectedProcedure.input(z.string().array()).query(async ({ input }) => {
return getDeepChildrenGoalIds(input).execute();
}),
});
2 changes: 2 additions & 0 deletions trpc/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { whatsnew } from './whatsnew';
import { crew } from './crew';
import { appConfig } from './appConfig';
import { project as projectV2 } from './projectV2';
import { goal as goalV2 } from './goalV2';

export const trpcRouter = router({
filter,
Expand All @@ -33,6 +34,7 @@ export const trpcRouter = router({
appConfig,
v2: router({
project: projectV2,
goal: goalV2,
}),
});

Expand Down
3 changes: 2 additions & 1 deletion trpc/router/router.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"These bindings is already exist": "These bindings is already exist",
"No filter with id": "No filter with id `{id}`",
"Cannot link goal to selected project": "Cannot link goal to selected project",
"Cannot transfer goal to selected project": "Cannot transfer goal to selected project"
"Cannot transfer goal to selected project": "Cannot transfer goal to selected project",
"Goal cannot be it's own criteria": "Goal cannot be it's own criteria"
}
3 changes: 2 additions & 1 deletion trpc/router/router.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"These bindings is already exist": "Такая связь уже сущевует",
"No filter with id": "Нет фильтра с id `{id}`",
"Cannot link goal to selected project": "Невозможно связать текущую цель с выбранным проектом",
"Cannot transfer goal to selected project": "Невозможно перенести цель в выбранный проект"
"Cannot transfer goal to selected project": "Невозможно перенести цель в выбранный проект",
"Goal cannot be it's own criteria": "Цель не может быть своим критерием"
}

0 comments on commit 1f405e6

Please sign in to comment.