Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Goal cannot be it's own criteria #2941

Merged
merged 2 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
42 changes: 42 additions & 0 deletions trpc/queries/goalV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,45 @@ 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', 'in', ids)
.where('GoalAchieveCriteria.deleted', 'is not', true)
.union((qb) =>
qb
.selectFrom('GoalAchieveCriteria')
.select('GoalAchieveCriteria.goalId as id')
.where('GoalAchieveCriteria.deleted', 'is not', true)
.innerJoin('parentsTree', 'parentsTree.id', 'GoalAchieveCriteria.criteriaGoalId'),
),
)
.selectFrom('parentsTree')
.selectAll();
};

export const getDeepChildrenGoalIds = (ids: string[]) => {
return db
.withRecursive('childrenTree', (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('childrenTree', 'childrenTree.id', 'GoalAchieveCriteria.goalId'),
),
)
.selectFrom('childrenTree')
.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: 'PRECONDITION_FAILED', 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": "Цель не может быть своим критерием"
}
Loading