diff --git a/src/components/ProjectAccessUser/ProjectAccessUser.tsx b/src/components/ProjectAccessUser/ProjectAccessUser.tsx index 808980288..26bf3dece 100644 --- a/src/components/ProjectAccessUser/ProjectAccessUser.tsx +++ b/src/components/ProjectAccessUser/ProjectAccessUser.tsx @@ -2,7 +2,7 @@ import { FC, useCallback, useMemo, useState } from 'react'; import { nullable } from '@taskany/bricks'; import { Fieldset, Switch, SwitchControl } from '@taskany/bricks/harmony'; -import { ProjectByIdReturnType } from '../../../trpc/inferredTypes'; +import { ProjectByIdReturnTypeV2 } from '../../../trpc/inferredTypes'; import { useProjectResource } from '../../hooks/useProjectResource'; import { ModalEvent, dispatchModalEvent } from '../../utils/dispatchModal'; import { SettingsCard, SettingsCardItem } from '../SettingsContent/SettingsContent'; @@ -12,7 +12,7 @@ import s from './ProjectAccessUser.module.css'; import { tr } from './ProjectAccessUser.i18n'; interface ProjectAccessUserProps { - project: NonNullable; + project: NonNullable; } export const ProjectAccessUser: FC = ({ project }) => { diff --git a/src/components/ProjectContext/ProjectContext.tsx b/src/components/ProjectContext/ProjectContext.tsx index 39d7cf5bc..6f29b0fae 100644 --- a/src/components/ProjectContext/ProjectContext.tsx +++ b/src/components/ProjectContext/ProjectContext.tsx @@ -1,7 +1,7 @@ import { createContext } from 'react'; -import { ProjectByIdReturnType } from '../../../trpc/inferredTypes'; +import { ProjectByIdReturnTypeV2 } from '../../../trpc/inferredTypes'; -export const ProjectContext = createContext<{ project: ProjectByIdReturnType }>({ +export const ProjectContext = createContext<{ project?: ProjectByIdReturnTypeV2 | null }>({ project: null, }); diff --git a/src/components/ProjectListItem/ProjectListItem.tsx b/src/components/ProjectListItem/ProjectListItem.tsx index 0d1795a26..3dcbc1c63 100644 --- a/src/components/ProjectListItem/ProjectListItem.tsx +++ b/src/components/ProjectListItem/ProjectListItem.tsx @@ -17,7 +17,7 @@ interface ProjectListItemProps { flowId: string; title: string; owner?: ActivityByIdReturnType; - participants?: ActivityByIdReturnType[]; + participants: ActivityByIdReturnType[] | null; starred?: boolean; watching?: boolean; averageScore: number | null; diff --git a/src/components/ProjectPage/ProjectPage.tsx b/src/components/ProjectPage/ProjectPage.tsx index 245b3f968..0665cb721 100644 --- a/src/components/ProjectPage/ProjectPage.tsx +++ b/src/components/ProjectPage/ProjectPage.tsx @@ -32,48 +32,45 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba preset, }); - const { data: project } = trpc.project.getById.useQuery( - { - id, - goalsQuery: queryState, - }, - { enabled: Boolean(id) }, - ); - - const { data: projectDeepInfo } = trpc.project.getDeepInfo.useQuery( - { - id, - goalsQuery: queryState, - }, - { - keepPreviousData: true, - staleTime: refreshInterval, - enabled: Boolean(id), - }, - ); - - const { data: projectTree } = trpc.v2.project.getProjectChildrenTree.useQuery({ - id, - goalsQuery: queryState, - }); + const [projectQuery, projectDeepInfoQuery, 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, + goalsQuery: queryState, + }, + { + keepPreviousData: true, + staleTime: refreshInterval, + enabled: Boolean(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]); + useEffect(() => { - const unsubUpdate = on('on:goal:update', () => { - utils.project.getById.invalidate(); - utils.project.getDeepInfo.invalidate(); - }); - const unsubDelete = on('on:goal:delete', () => { - utils.project.getById.invalidate(); - utils.project.getDeepInfo.invalidate(); - }); + const unsubUpdate = on('on:goal:update', invalidateFnsCallback); + const unsubDelete = on('on:goal:delete', invalidateFnsCallback); return () => { unsubUpdate(); unsubDelete(); }; - }, [on, utils.project.getDeepInfo, utils.project.getById]); + }, [on, invalidateFnsCallback]); const handleItemEnter = useCallback( (goal: NonNullable) => { @@ -82,7 +79,7 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba [setPreview], ); - const ctx = useMemo(() => ({ project: project ?? null }), [project]); + const ctx = useMemo(() => ({ project: projectQuery.data ?? null }), [projectQuery.data]); return ( @@ -92,25 +89,25 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba scrollerShadow={view === 'kanban' ? 70 : 0} title={tr .raw('title', { - project: project?.title, + project: ctx.project?.title, }) .join('')} header={ - + } > - {nullable(project?.parent, (p) => ( + {nullable(ctx.project?.parent, (p) => ( {p.map((item) => ( @@ -122,18 +119,18 @@ export const ProjectPage = ({ user, ssrTime, params: { id }, defaultPresetFallba ))} - - {nullable(project, (p) => ( + {nullable(ctx.project, (p) => ( + - ))} - + + ))} diff --git a/src/components/ProjectParticipants/ProjectParticipants.tsx b/src/components/ProjectParticipants/ProjectParticipants.tsx index 35b8380da..69ce2b343 100644 --- a/src/components/ProjectParticipants/ProjectParticipants.tsx +++ b/src/components/ProjectParticipants/ProjectParticipants.tsx @@ -9,11 +9,11 @@ import { tr } from './ProjectParticipants.i18n'; interface ProjectParticipantsProps { id: string; - participants: ComponentProps['users']; + participants: ComponentProps['users'] | null; } export const ProjectParticipants: FC = ({ id, participants }) => { - const filterIds = useMemo(() => participants.map(({ id }) => id), [participants]); + const filterIds = useMemo(() => (participants ?? []).map(({ id }) => id), [participants]); const { onProjectParticipantAdd, onProjectParticipantRemove } = useProjectResource(id); const onAdd = useCallback( diff --git a/src/components/ProjectSettingsPage/ProjectSettingsPage.tsx b/src/components/ProjectSettingsPage/ProjectSettingsPage.tsx index a1423f64c..e07efcaed 100644 --- a/src/components/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/src/components/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -70,7 +70,7 @@ const ModalOnEvent = dynamic(() => import('../ModalOnEvent')); export const ProjectSettingsPage = ({ user, ssrTime, params: { id } }: ExternalPageProps) => { const router = useRouter(); - const project = trpc.project.getById.useQuery({ id }); + const project = trpc.v2.project.getById.useQuery({ id, includeChildren: true }); const { updateProject, deleteProject, transferOwnership } = useProjectResource(id); const { data: childrenIds = [] } = trpc.v2.project.deepChildrenIds.useQuery({ in: [{ id }] }); @@ -86,6 +86,14 @@ export const ProjectSettingsPage = ({ user, ssrTime, params: { id } }: ExternalP mode: 'onChange', reValidateMode: 'onChange', shouldFocusError: true, + values: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: project.data!.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + title: project.data!.title, + description: project.data?.description, + parent: project.data?.parent, + }, defaultValues: { id: project.data?.id, title: project.data?.title, diff --git a/src/components/ProjectTeamPage/ProjectTeamPage.tsx b/src/components/ProjectTeamPage/ProjectTeamPage.tsx index da6d90fc0..850b1b376 100644 --- a/src/components/ProjectTeamPage/ProjectTeamPage.tsx +++ b/src/components/ProjectTeamPage/ProjectTeamPage.tsx @@ -20,7 +20,7 @@ import { tr } from './ProjectTeamPage.i18n'; import s from './ProjectTeamPage.module.css'; export const ProjectTeamPage = ({ user, ssrTime, params: { id } }: ExternalPageProps) => { - const { data: project } = trpc.project.getById.useQuery({ id }); + const { data: project } = trpc.v2.project.getById.useQuery({ id }); const { updateProjectTeams } = useProjectResource(id); const ids = useMemo(() => project?.teams.map(({ externalTeamId }) => externalTeamId) ?? [], [project]); diff --git a/src/components/TeamComboBox/TeamComboBox.tsx b/src/components/TeamComboBox/TeamComboBox.tsx index a9de0aa84..ef77180e2 100644 --- a/src/components/TeamComboBox/TeamComboBox.tsx +++ b/src/components/TeamComboBox/TeamComboBox.tsx @@ -4,14 +4,14 @@ import { IconPlusCircleOutline } from '@taskany/icons'; import { trpc } from '../../utils/trpcClient'; import { useProjectResource } from '../../hooks/useProjectResource'; -import { ProjectByIdReturnType, TeamSuggetionsReturnType } from '../../../trpc/inferredTypes'; +import { ProjectByIdReturnTypeV2, TeamSuggetionsReturnType } from '../../../trpc/inferredTypes'; import { Dropdown, DropdownPanel, DropdownTrigger } from '../Dropdown/Dropdown'; interface TeamComboBoxProps { text?: React.ComponentProps['text']; placeholder?: string; disabled?: boolean; - project: NonNullable; + project: NonNullable; } export const TeamComboBox: FC> = ({ diff --git a/src/components/UserEditableList/UserEditableList.tsx b/src/components/UserEditableList/UserEditableList.tsx index ecfeb065e..49f7811ce 100644 --- a/src/components/UserEditableList/UserEditableList.tsx +++ b/src/components/UserEditableList/UserEditableList.tsx @@ -13,7 +13,7 @@ import { tr } from './UserEditableList.i18n'; export const UserEditableList: FC<{ editable?: boolean; - users: { id: string; user: User | null }[]; + users: { id: string; user: User | null }[] | null; filterIds: string[]; onRemove: (id: string) => void; onAdd: (id: string) => void; diff --git a/src/hooks/useProjectResource.ts b/src/hooks/useProjectResource.ts index 3e2a99e07..4bcdc982f 100644 --- a/src/hooks/useProjectResource.ts +++ b/src/hooks/useProjectResource.ts @@ -21,8 +21,8 @@ export const useProjectResource = (id: string) => { const removeParticipants = trpc.project.removeParticipants.useMutation(); const invalidate = useCallback(() => { - utils.project.getById.invalidate({ id }); - }, [id, utils.project.getById]); + utils.v2.project.getById.invalidate({ id }); + }, [id, utils.v2.project.getById]); const createProject = useCallback( (cb: Callback) => async (form: ProjectCreate) => { diff --git a/src/pages/projects/[id]/index.tsx b/src/pages/projects/[id]/index.tsx index ca918973c..03206bb32 100644 --- a/src/pages/projects/[id]/index.tsx +++ b/src/pages/projects/[id]/index.tsx @@ -15,17 +15,13 @@ export const getServerSideProps = declareSsrProps( } = props; try { - const project = await ssrHelpers.project.getById.fetch({ id, goalsQuery: queryState }); + const project = await ssrHelpers.v2.project.getById.fetch({ id }); if (!project) { throw new TRPCError({ code: 'NOT_FOUND' }); } await Promise.all([ - ssrHelpers.project.getDeepInfo.fetch({ - id, - goalsQuery: queryState, - }), ssrHelpers.v2.project.getProjectChildrenTree.fetch({ id, goalsQuery: queryState, diff --git a/src/pages/projects/[id]/settings.tsx b/src/pages/projects/[id]/settings.tsx index 5082624ef..ba9502b3e 100644 --- a/src/pages/projects/[id]/settings.tsx +++ b/src/pages/projects/[id]/settings.tsx @@ -7,7 +7,7 @@ import { declareSsrProps } from '../../../utils/declareSsrProps'; export const getServerSideProps = declareSsrProps( async ({ ssrHelpers, params: { id } }) => { try { - const project = await ssrHelpers.project.getById.fetch({ id }); + const project = await ssrHelpers.v2.project.getById.fetch({ id, includeChildren: true }); await ssrHelpers.v2.project.deepChildrenIds.fetch({ in: [{ id }] }); if (!project) { diff --git a/src/pages/projects/[id]/team.tsx b/src/pages/projects/[id]/team.tsx index a8284a248..0d0b27812 100644 --- a/src/pages/projects/[id]/team.tsx +++ b/src/pages/projects/[id]/team.tsx @@ -6,7 +6,7 @@ import { declareSsrProps } from '../../../utils/declareSsrProps'; export const getServerSideProps = declareSsrProps( async ({ ssrHelpers, params: { id } }) => { try { - const project = await ssrHelpers.project.getById.fetch({ id }); + const project = await ssrHelpers.v2.project.getById.fetch({ id }); if (!project || !process.env.NEXT_PUBLIC_CREW_URL) { throw new TRPCError({ code: 'NOT_FOUND' }); diff --git a/src/schema/project.ts b/src/schema/project.ts index 08ffb1e91..7c493be88 100644 --- a/src/schema/project.ts +++ b/src/schema/project.ts @@ -72,7 +72,7 @@ export const projectUpdateSchema = z.object({ title: z.string(), }), ) - .optional(), + .nullish(), accessUsers: z .array( z.object({ diff --git a/src/utils/recalculateCriteriaScore.ts b/src/utils/recalculateCriteriaScore.ts index 8cfcc27e2..fda52530b 100644 --- a/src/utils/recalculateCriteriaScore.ts +++ b/src/utils/recalculateCriteriaScore.ts @@ -29,7 +29,7 @@ export const baseCalcCriteriaWeight = < criteriaList: T[], ): number => { let achivedWithWeight = 0; - let comletedWithoutWeight = 0; + let completedWithoutWeight = 0; let anyWithoutWeight = 0; let allWeight = 0; @@ -47,7 +47,7 @@ export const baseCalcCriteriaWeight = < achivedWithWeight += weight; if (!weight) { - comletedWithoutWeight += 1; + completedWithoutWeight += 1; } } } @@ -67,7 +67,7 @@ export const baseCalcCriteriaWeight = < } return Math.min( - achivedWithWeight + Math.ceil(quantityByWeightlessCriteria * comletedWithoutWeight), + achivedWithWeight + Math.ceil(quantityByWeightlessCriteria * completedWithoutWeight), maxPossibleCriteriaWeight, ); }; diff --git a/trpc/inferredTypes.ts b/trpc/inferredTypes.ts index 818ee181e..40cf0a12c 100644 --- a/trpc/inferredTypes.ts +++ b/trpc/inferredTypes.ts @@ -27,6 +27,7 @@ export type StateType = State['type']; export type DashboardProjectV2 = RouterOutputs['v2']['project']['getUserDashboardProjects']['groups'][number]; export type DashboardGoalV2 = NonNullable[number]; +export type ProjectByIdReturnTypeV2 = RouterOutputs['v2']['project']['getById']; export type GoalActivityHistory = RouterOutputs['goal']['getGoalActivityFeed']; export type GoalComments = RouterOutputs['goal']['getGoalCommentsFeed']; diff --git a/trpc/queries/activity.ts b/trpc/queries/activity.ts index e36918661..47f20f241 100644 --- a/trpc/queries/activity.ts +++ b/trpc/queries/activity.ts @@ -18,3 +18,14 @@ export const getUserActivity = () => { .select([sql`"User"`.as('user'), sql`"Ghost"`.as('ghost')]) .$castTo(); }; + +export const getAccessUsersByProjectId = ({ projectId }: { projectId: string }) => { + return db + .selectFrom('_projectAccess') + .innerJoinLateral( + () => getUserActivity().as('activity'), + (join) => join.onRef('activity.id', '=', '_projectAccess.A'), + ) + .selectAll('activity') + .where('_projectAccess.B', '=', projectId); +}; diff --git a/trpc/queries/goalV2.ts b/trpc/queries/goalV2.ts index b563d1b5c..d0e2fe040 100644 --- a/trpc/queries/goalV2.ts +++ b/trpc/queries/goalV2.ts @@ -91,7 +91,7 @@ export const getGoalsQuery = (params: GetGoalsQueryParams) => ({ selectFrom }) => selectFrom('GoalAchieveCriteria') .distinctOn('GoalAchieveCriteria.id') - .leftJoin('Goal as criteriaGoal', 'GoalAchieveCriteria.criteriaGoalId', 'Goal.id') + .leftJoin('Goal as criteriaGoal', 'GoalAchieveCriteria.criteriaGoalId', 'criteriaGoal.id') .selectAll('GoalAchieveCriteria') .select([sql`"criteriaGoal"`.as('criteriaGoal')]) .where('GoalAchieveCriteria.deleted', 'is not', true) diff --git a/trpc/queries/projectV2.ts b/trpc/queries/projectV2.ts index c275e0554..1b5a861ae 100644 --- a/trpc/queries/projectV2.ts +++ b/trpc/queries/projectV2.ts @@ -755,3 +755,115 @@ export const getProjectChildrenTreeQuery = ({ id, goalsQuery }: { id: string; go .groupBy(['Project.id', 'ch.level', 'ch.parent_chain']) .orderBy('ch.level asc'); }; + +export const getProjectById = ({ id, ...user }: { id: string; activityId: string; role: Role }) => { + return db + .with('calculatedFields', (qb) => + qb + .selectFrom('Project') + .leftJoinLateral( + ({ selectFrom }) => + selectFrom('Project') + .selectAll('Project') + .where('Project.id', 'in', () => getParentProjectsId({ in: [{ id }] })) + .as('parent'), + (join) => join.onTrue(), + ) + .leftJoinLateral( + () => getUserActivity().distinctOn('Activity.id').as('participant'), + (join) => + join.onRef('participant.id', 'in', (qb) => + qb.selectFrom('_projectParticipants').select('A').whereRef('B', '=', 'Project.id'), + ), + ) + .leftJoinLateral( + (qb) => + qb + .selectFrom('Team') + .selectAll('Team') + .where('Team.id', 'in', ({ selectFrom }) => + selectFrom('_projects') + .select('_projects.B') + .whereRef('_projects.A', '=', 'Project.id'), + ) + .as('teams'), + (join) => join.onTrue(), + ) + .selectAll('Project') + .select(({ fn, exists, selectFrom, val, case: caseFn }) => [ + caseFn() + .when(fn.count('parent.id'), '>', 0) + .then(fn.agg('array_agg', [fn.toJson('parent')])) + .else(null) + .end() + .as('parent'), + caseFn() + .when(fn.count('teams.id'), '>', 0) + .then(fn.agg('array_agg', [fn.toJson('teams')])) + .else(null) + .end() + .as('teams'), + caseFn() + .when(fn.count('participant.id'), '>', 0) + .then(fn.agg('array_agg', [fn.toJson('participant')])) + .else(null) + .end() + .as('participants'), + sql`("Project"."activityId" = ${val(user.activityId)})`.as('_isOwner'), + exists( + selectFrom('_projectParticipants') + .select('A') + .whereRef('B', '=', 'Project.id') + .where('A', '=', user.activityId), + ) + .$castTo() + .as('_isParticipant'), + exists( + selectFrom('_projectWatchers') + .select('B') + .where('A', '=', user.activityId) + .whereRef('B', '=', 'Project.id'), + ) + .$castTo() + .as('_isWatching'), + exists( + selectFrom('_projectStargizers') + .select('B') + .where('A', '=', user.activityId) + .whereRef('B', '=', 'Project.id'), + ) + .$castTo() + .as('_isStarred'), + jsonBuildObject({ + stargizers: sql`(select count("A") from "_projectStargizers" where "B" = "Project".id)`, + watchers: sql`(select count("A") from "_projectWatchers" where "B" = "Project".id)`, + children: sql`(select count("B") from "_parentChildren" where "A" = "Project".id)`, + participants: sql`(select count("A") from "_projectParticipants" where "B" = "Project".id)`, + goals: sql`(select count("Goal".id) from "Goal" where "Goal"."projectId" = "Project".id and "Goal"."archived" is not true)`, + }).as('_count'), + ]) + .where('Project.id', '=', id) + .groupBy('Project.id'), + ) + .selectFrom('calculatedFields as project') + .innerJoinLateral( + () => getUserActivity().as('activity'), + (join) => join.onRef('activity.id', '=', 'project.activityId'), + ) + .selectAll('project') + .select(({ fn, val }) => [ + fn.toJson('activity').as('activity'), + sql`((${val(user.role === Role.ADMIN)} or "project"."activityId" = ${val( + user.activityId, + )} or "project"."_isParticipant") and not "project"."personal")`.as('_isEditable'), + ]); +}; + +export const getChildrenProjectByParentProjectId = ({ id }: { id: string }) => { + return db + .selectFrom('Project') + .select(['Project.id', 'Project.title']) + .where('Project.id', 'in', ({ selectFrom }) => + selectFrom('_parentChildren').select('_parentChildren.B').where('_parentChildren.A', '=', id), + ); +}; diff --git a/trpc/router/projectV2.ts b/trpc/router/projectV2.ts index a1d426d2f..e5b910bac 100644 --- a/trpc/router/projectV2.ts +++ b/trpc/router/projectV2.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { Role } from '@prisma/client'; +import { TRPCError } from '@trpc/server'; import { router, protectedProcedure } from '../trpcBackend'; import { projectsChildrenIdsSchema, projectSuggestionsSchema, userProjectsSchema } from '../../src/schema/project'; @@ -13,6 +14,8 @@ import { getDeepChildrenProjectsId, getAllProjectsQuery, getProjectChildrenTreeQuery, + getProjectById, + getChildrenProjectByParentProjectId, } from '../queries/projectV2'; import { queryWithFiltersSchema, sortableProjectsPropertiesArraySchema } from '../../src/schema/common'; import { @@ -25,10 +28,13 @@ import { Ghost, Activity, Priority, + Team, } from '../../generated/kysely/types'; import { ExtractTypeFromGenerated, pickUniqueValues } from '../utils'; import { baseCalcCriteriaWeight } from '../../src/utils/recalculateCriteriaScore'; import { getGoalsQuery } from '../queries/goalV2'; +import { projectAccessMiddleware } from '../access/accessMiddlewares'; +import { getAccessUsersByProjectId } from '../queries/activity'; type ProjectActivity = ExtractTypeFromGenerated & { user: ExtractTypeFromGenerated | null; @@ -41,7 +47,7 @@ type ProjectResponse = ExtractTypeFromGenerated & { _isOwner: boolean; _isEditable: boolean; activity: ProjectActivity; - participants: ProjectActivity[]; + participants: ProjectActivity[] | null; goals?: any[]; // this prop is overrides below children: ExtractTypeFromGenerated[] | null; }; @@ -107,6 +113,14 @@ export interface ProjectTreeRow { deep: number; } +type ProjectById = Omit & + Pick & { + parent: Array<{ id: string; title: string }>; + accessUsers: Array; + teams: Array; + children?: Array<{ id: string; title: string }>; + }; + export const project = router({ suggestions: protectedProcedure .input(projectSuggestionsSchema) @@ -344,6 +358,41 @@ export const project = router({ }, ), + getById: protectedProcedure + .input( + z.object({ + id: z.string(), + includeChildren: z.boolean().optional(), + goalsQuery: queryWithFiltersSchema.optional(), + }), + ) + .use(projectAccessMiddleware) + .query(async ({ input, ctx }) => { + const { id } = input; + + const [project, accessUsers, children = null] = await Promise.all([ + getProjectById({ + ...ctx.session.user, + id, + }) + .$castTo() + .executeTakeFirst(), + getAccessUsersByProjectId({ projectId: id }).execute(), + input.includeChildren ? getChildrenProjectByParentProjectId({ id }).execute() : Promise.resolve(null), + ]); + + if (project == null) { + throw new TRPCError({ code: 'NOT_FOUND' }); + } + + return { + ...project, + ...(children != null ? { children } : undefined), + parent: pickUniqueValues(project.parent, 'id'), + accessUsers, + }; + }), + getProjectGoalsById: protectedProcedure .input( z.object({ @@ -371,7 +420,7 @@ export const project = router({ if (goal.criteria != null) { const uniqCriteria = pickUniqueValues(goal.criteria, 'id') as NonNullable; goal._achivedCriteriaWeight = baseCalcCriteriaWeight(uniqCriteria); - delete goal.criteria; + goal.criteria = uniqCriteria; } }