From eb99905a2e7d66b696b25a9d08abb02c286ccf40 Mon Sep 17 00:00:00 2001 From: Goriany Igor Date: Thu, 12 Sep 2024 17:00:35 +0300 Subject: [PATCH] feat: add option to sort projects --- .../DashboardPage/DashboardPage.tsx | 4 +- .../FiltersPanel/FiltersPanel.i18n/en.json | 3 +- .../FiltersPanel/FiltersPanel.i18n/ru.json | 3 +- src/components/FiltersPanel/FiltersPanel.tsx | 56 ++++++++++++-- src/components/GoalsPage/GoalsPage.tsx | 1 + src/components/GroupedGoalList.tsx | 3 +- src/components/SortList/SortList.i18n/en.json | 5 +- src/components/SortList/SortList.i18n/ru.json | 5 +- src/components/SortList/SortList.tsx | 58 ++++++++++---- src/hooks/useUrlFilterParams.ts | 76 ++++++++++++------- src/schema/common.ts | 30 +++++++- trpc/queries/projectV2.ts | 57 ++++++++++++-- trpc/router/projectV2.ts | 10 ++- 13 files changed, 246 insertions(+), 65 deletions(-) diff --git a/src/components/DashboardPage/DashboardPage.tsx b/src/components/DashboardPage/DashboardPage.tsx index f2959fc21..01c8b30e9 100644 --- a/src/components/DashboardPage/DashboardPage.tsx +++ b/src/components/DashboardPage/DashboardPage.tsx @@ -23,7 +23,7 @@ import { tr } from './DashboardPage.i18n'; export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: ExternalPageProps) => { const { preset } = useFiltersPreset({ defaultPresetFallback }); - const { currentPreset, queryState } = useUrlFilterParams({ + const { currentPreset, queryState, projectsSort } = useUrlFilterParams({ preset, }); @@ -33,6 +33,7 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External ...queryState, limit: 10, }, + projectsSort, }, { getNextPageParam: ({ pagination }) => pagination.offset, @@ -85,6 +86,7 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External counter={goalsCount} filterPreset={preset} enableLayoutToggle + enableProjectsSort enableHideProjectToggle /> } diff --git a/src/components/FiltersPanel/FiltersPanel.i18n/en.json b/src/components/FiltersPanel/FiltersPanel.i18n/en.json index e5659f513..6e84d6fbe 100644 --- a/src/components/FiltersPanel/FiltersPanel.i18n/en.json +++ b/src/components/FiltersPanel/FiltersPanel.i18n/en.json @@ -7,7 +7,8 @@ "Estimate": "Estimate", "Limit": "Limit", "Preset": "Preset", - "Sort": "Sort", + "Goals sort": "Goals sort", + "Projects sort": "Projects sort", "Issuer": "Issuer", "Participant": "Participant", "Filter": "Filter", diff --git a/src/components/FiltersPanel/FiltersPanel.i18n/ru.json b/src/components/FiltersPanel/FiltersPanel.i18n/ru.json index c38778dd0..02294d6ca 100644 --- a/src/components/FiltersPanel/FiltersPanel.i18n/ru.json +++ b/src/components/FiltersPanel/FiltersPanel.i18n/ru.json @@ -7,7 +7,8 @@ "Estimate": "Срок", "Limit": "Лимит", "Preset": "Пресет", - "Sort": "Сортировка", + "Goals sort": "Сортировка целей", + "Projects sort": "Сортировка проектов", "Issuer": "Автор", "Participant": "Участник", "Filter": "Фильтр", diff --git a/src/components/FiltersPanel/FiltersPanel.tsx b/src/components/FiltersPanel/FiltersPanel.tsx index bb515da5d..052d092f7 100644 --- a/src/components/FiltersPanel/FiltersPanel.tsx +++ b/src/components/FiltersPanel/FiltersPanel.tsx @@ -10,7 +10,13 @@ import { } from '@taskany/bricks/harmony'; import { nullable, useLatest } from '@taskany/bricks'; -import { FilterQueryState, QueryState, useUrlFilterParams } from '../../hooks/useUrlFilterParams'; +import { + FilterQueryState, + QueryState, + SortableGoalsProps, + SortableProjectsProps, + useUrlFilterParams, +} from '../../hooks/useUrlFilterParams'; import { FilterById } from '../../../trpc/inferredTypes'; import { filtersPanel, @@ -52,8 +58,9 @@ export const FiltersPanel: FC<{ filterPreset?: FilterById; enableViewToggle?: boolean; enableLayoutToggle?: boolean; - enableHideProjectToggle?: boolean; children?: ReactNode; + enableHideProjectToggle?: boolean; + enableProjectsSort?: boolean; }> = memo( ({ children, @@ -62,6 +69,7 @@ export const FiltersPanel: FC<{ counter = 0, enableViewToggle, enableLayoutToggle, + enableProjectsSort, enableHideProjectToggle, filterPreset, }) => { @@ -72,8 +80,10 @@ export const FiltersPanel: FC<{ currentPreset, queryString, queryState, + projectsSort, resetQueryState, batchQueryState, + setProjectsSortFilter, setSortFilter, queryFilterState, groupBy, @@ -242,7 +252,7 @@ export const FiltersPanel: FC<{ ))} - {tr('Sort')} + {tr('Goals sort')} key === k); if (paramExistingIndex > -1) { - sortParams[paramExistingIndex] = { key, dir }; + sortParams[paramExistingIndex] = { + key: key as SortableGoalsProps, + dir, + }; } else { - sortParams.push({ key, dir }); + sortParams.push({ key: key as SortableGoalsProps, dir }); } } @@ -265,6 +278,39 @@ export const FiltersPanel: FC<{ }} /> + {nullable(enableProjectsSort, () => ( + <> + {tr('Projects sort')} + + { + let sortParams = (projectsSort ?? []).slice(); + + if (!dir) { + sortParams = sortParams.filter(({ key: k }) => key !== k); + } else { + const paramExistingIndex = sortParams.findIndex( + ({ key: k }) => key === k, + ); + + if (paramExistingIndex > -1) { + sortParams[paramExistingIndex] = { + key: key as SortableProjectsProps, + dir, + }; + } else { + sortParams.push({ key: key as SortableProjectsProps, dir }); + } + } + + setProjectsSortFilter(sortParams); + }} + /> + + + ))} {tr('Visibility')} } > diff --git a/src/components/GroupedGoalList.tsx b/src/components/GroupedGoalList.tsx index 9cf428c56..402059fa8 100644 --- a/src/components/GroupedGoalList.tsx +++ b/src/components/GroupedGoalList.tsx @@ -21,7 +21,7 @@ export const projectsSize = 20; export const GroupedGoalList: React.FC = ({ filterPreset }) => { const { setPreview, on } = useGoalPreview(); - const { queryState } = useUrlFilterParams({ + const { queryState, projectsSort } = useUrlFilterParams({ preset: filterPreset, }); @@ -31,6 +31,7 @@ export const GroupedGoalList: React.FC = ({ filterPreset } limit: projectsSize, goalsQuery: queryState, firstLevel: !!queryState?.project?.length, + projectsSort, }, { getNextPageParam: ({ pagination }) => pagination.offset, diff --git a/src/components/SortList/SortList.i18n/en.json b/src/components/SortList/SortList.i18n/en.json index b519dee8e..fe4f257bc 100644 --- a/src/components/SortList/SortList.i18n/en.json +++ b/src/components/SortList/SortList.i18n/en.json @@ -6,5 +6,8 @@ "Activity": "Activity", "Owner": "Owner", "UpdatedAt": "Updated", - "CreatedAt": "Created" + "CreatedAt": "Created", + "Stargizers": "Stargizers", + "Goals": "Goals", + "Watchers": "Watchers" } diff --git a/src/components/SortList/SortList.i18n/ru.json b/src/components/SortList/SortList.i18n/ru.json index 250074ca5..0cfc060e6 100644 --- a/src/components/SortList/SortList.i18n/ru.json +++ b/src/components/SortList/SortList.i18n/ru.json @@ -6,5 +6,8 @@ "Activity": "Автор", "Owner": "Ответственный", "UpdatedAt": "Обновлено", - "CreatedAt": "Создано" + "CreatedAt": "Создано", + "Stargizers": "В избранном", + "Goals": "Цели", + "Watchers": "Отслеживается" } diff --git a/src/components/SortList/SortList.tsx b/src/components/SortList/SortList.tsx index ee04b89cd..e679de46b 100644 --- a/src/components/SortList/SortList.tsx +++ b/src/components/SortList/SortList.tsx @@ -1,52 +1,80 @@ import React, { useCallback, useMemo } from 'react'; import { AutoComplete, AutoCompleteList } from '@taskany/bricks/harmony'; -import type { FilterQueryState, SortableProps, SortDirection } from '../../hooks/useUrlFilterParams'; +import type { + FilterQueryState, + SortableBaseProps, + SortableGoalsProps, + SortableProjectsProps, + SortDirection, +} from '../../hooks/useUrlFilterParams'; import { SortButton } from '../SortButton/SortButton'; import { tr } from './SortList.i18n'; import styles from './SortList.module.css'; -interface SortListProps { - value?: FilterQueryState['sort']; - onChange: (key: SortableProps, dir: SortDirection | null) => void; +interface SortListProps { + variant?: 'goals' | 'projects'; + value?: T; + onChange: (key: SortableGoalsProps | SortableProjectsProps, dir: SortDirection | null) => void; } interface SingleSortItem { - id: SortableProps; + id: SortableGoalsProps | SortableProjectsProps; title: string; dir: SortDirection | null; } -export const SortList: React.FC = ({ value, onChange }) => { +export const SortList = ({ + variant = 'goals', + value, + onChange, +}: SortListProps) => { const { itemsToRender, sortItems } = useMemo(() => { - const sortItems = { + const baseSortItems: Record = { title: tr('Title'), - state: tr('State'), - priority: tr('Priority'), - project: tr('Project'), - activity: tr('Activity'), owner: tr('Owner'), updatedAt: tr('UpdatedAt'), createdAt: tr('CreatedAt'), }; + const sortGoalsItems: Record, string> = { + state: tr('State'), + activity: tr('Activity'), + priority: tr('Priority'), + project: tr('Project'), + }; + + const sortProjectItems: Record, string> = { + stargizers: tr('Stargizers'), + watchers: tr('Watchers'), + goals: tr('Goals'), + }; + + const sortItems = + variant === 'goals' ? { ...baseSortItems, ...sortGoalsItems } : { ...baseSortItems, ...sortProjectItems }; return { sortItems, - itemsToRender: (Object.entries(sortItems) as Array<[SortableProps, string]>).map(([id, title]) => ({ + itemsToRender: ( + Object.entries(sortItems) as Array<[SortableGoalsProps | SortableProjectsProps, string]> + ).map(([id, title]) => ({ id, title, dir: null, })), }; - }, []); + }, [variant]); const selected: SingleSortItem[] | undefined = useMemo(() => { - return value?.map(({ key, dir }) => ({ id: key, title: sortItems[key], dir })); + return value?.map(({ key, dir }) => ({ + id: key, + title: sortItems[key as SortableBaseProps], + dir, + })); }, [value, sortItems]); const handleChange = useCallback( - (key: SortableProps) => (dir: SortDirection | null) => { + (key: SortableGoalsProps | SortableProjectsProps) => (dir: SortDirection | null) => { onChange(key, dir); }, [onChange], diff --git a/src/hooks/useUrlFilterParams.ts b/src/hooks/useUrlFilterParams.ts index 703a1c838..71194b671 100644 --- a/src/hooks/useUrlFilterParams.ts +++ b/src/hooks/useUrlFilterParams.ts @@ -3,11 +3,11 @@ import { ParsedUrlQuery } from 'querystring'; import { MouseEventHandler, useCallback, useMemo, useState } from 'react'; import { FilterById, StateType } from '../../trpc/inferredTypes'; -import { StateTypeEnum } from '../schema/common'; +import { SortableProjectsPropertiesArray, StateTypeEnum } from '../schema/common'; import { setCookie } from '../utils/cookies'; export type SortDirection = 'asc' | 'desc'; -export type SortableProps = +export type SortableGoalsProps = | 'title' | 'state' | 'priority' @@ -17,6 +17,10 @@ export type SortableProps = | 'updatedAt' | 'createdAt'; +export type SortableProjectsProps = NonNullable[number]['key']; + +export type SortableBaseProps = SortableGoalsProps & SortableProjectsProps; + export const filtersNoSearchPresetCookie = 'taskany.NoSearchPreset'; // TODO: replace it with QueryWithFilters from schema/common @@ -32,7 +36,8 @@ export interface FilterQueryState { project: string[]; partnershipProject: string[]; query: string; - sort: Array<{ key: SortableProps; dir: SortDirection }>; + sort: Array<{ key: SortableGoalsProps; dir: SortDirection }>; + projectsSort: NonNullable; hideCriteria?: boolean; hideEmptyProjects?: boolean; } @@ -86,16 +91,20 @@ export interface QueryState extends BaseQueryState, FilterQueryState {} const parseQueryParam = (param = '') => param.split(',').filter(Boolean); -const parseSortQueryParam = (param = '') => +const parseSortQueryParam = (param = '') => param.split(',').reduce((acc, curr) => { if (curr) { - const [key, dir] = curr.split(':') as [SortableProps, SortDirection]; - acc.push({ key, dir }); + const [key, dir] = curr.split(':') as [ + K extends 'goals' ? SortableGoalsProps : SortableProjectsProps, + SortDirection, + ]; + acc.push({ key: key as SortableBaseProps, dir }); } return acc; - }, [] as QueryState['sort']); + }, [] as K extends 'goals' ? QueryState['sort'] : QueryState['projectsSort']); -const stringifySortQueryParam = (param: QueryState['sort']) => param.map(({ key, dir }) => `${key}:${dir}`).join(','); +const stringifySortQueryParam = (param: QueryState['sort' | 'projectsSort']) => + param.map(({ key, dir }) => `${key}:${dir}`).join(','); export const buildURLSearchParams = ({ priority = [], @@ -112,6 +121,7 @@ export const buildURLSearchParams = ({ starred, watching, sort = [], + projectsSort = [], groupBy, view, limit, @@ -146,6 +156,10 @@ export const buildURLSearchParams = ({ sort.length > 0 ? urlParams.set('sort', stringifySortQueryParam(sort)) : urlParams.delete('sort'); + projectsSort.length > 0 + ? urlParams.set('projectsSort', stringifySortQueryParam(projectsSort)) + : urlParams.delete('projectsSort'); + query.length > 0 ? urlParams.set('query', query.toString()) : urlParams.delete('query'); starred ? urlParams.set('starred', '1') : urlParams.delete('starred'); @@ -190,7 +204,10 @@ export const parseFilterValues = (query: ParsedUrlQuery): FilterQueryState => { if (query.partnershipProject) queryMap.partnershipProject = parseQueryParam(query.partnershipProject?.toString()); if (query.query) queryMap.query = parseQueryParam(query.query?.toString()).toString(); if (query.sort) { - queryMap.sort = parseSortQueryParam(query.sort?.toString()); + queryMap.sort = parseSortQueryParam<'goals'>(query.sort?.toString()); + } + if (query.projectsSort) { + queryMap.projectsSort = parseSortQueryParam<'projects'>(query.projectsSort?.toString()); } if (query.hideCriteria) queryMap.hideCriteria = Boolean(query.hideCriteria) || undefined; if (query.hideEmptyProjects) queryMap.hideEmptyProjects = Boolean(query.hideEmptyProjects) || undefined; @@ -214,24 +231,26 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => { const router = useRouter(); const [currentPreset, setCurrentPreset] = useState(preset); const [prevPreset, setPrevPreset] = useState(preset); - const { queryState, queryFilterState, groupBy, hideCriteria, hideEmptyProjects, view } = useMemo(() => { - const query = currentPreset ? Object.fromEntries(new URLSearchParams(currentPreset.params)) : router.query; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { groupBy, view, id, ...queries } = query; - - const { queryState = undefined, queryFilterState = undefined } = Object.keys(queries).length - ? parseQueryState({ groupBy, view, ...queries }) - : {}; - - return { - queryFilterState, - queryState, - groupBy: groupBy as GroupByParam | undefined, - view: view as PageView | undefined, - hideCriteria: queryState?.hideCriteria, - hideEmptyProjects: queryState?.hideEmptyProjects, - }; - }, [router.query, currentPreset]); + const { queryState, queryFilterState, projectsSort, groupBy, hideCriteria, hideEmptyProjects, view } = + useMemo(() => { + const query = currentPreset ? Object.fromEntries(new URLSearchParams(currentPreset.params)) : router.query; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { groupBy, view, id, ...queries } = query; + + const { queryState = undefined, queryFilterState = undefined } = Object.keys(queries).length + ? parseQueryState({ groupBy, view, ...queries }) + : {}; + + return { + queryFilterState, + queryState, + groupBy: groupBy as GroupByParam | undefined, + view: view as PageView | undefined, + hideCriteria: queryState?.hideCriteria, + projectsSort: queryState?.projectsSort, + hideEmptyProjects: queryState?.hideEmptyProjects, + }; + }, [router.query, currentPreset]); const queryString = router.asPath.split('?')[1]; @@ -312,6 +331,7 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => { watching: false, query: '', sort: [], + projectsSort: [], groupBy: undefined, view: undefined, hideCriteria: undefined, @@ -368,6 +388,7 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => { setStarredFilter: pushStateProvider.key('starred'), setWatchingFilter: pushStateProvider.key('watching'), setSortFilter: pushStateProvider.key('sort'), + setProjectsSortFilter: pushStateProvider.key('projectsSort'), setFulltextFilter: pushStateProvider.key('query'), setLimitFilter: pushStateProvider.key('limit'), setGroupBy: pushStateProvider.key('groupBy'), @@ -381,6 +402,7 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => { return { queryState, + projectsSort, queryFilterState, queryString, currentPreset, diff --git a/src/schema/common.ts b/src/schema/common.ts index fe08ecb94..029a890aa 100644 --- a/src/schema/common.ts +++ b/src/schema/common.ts @@ -11,17 +11,39 @@ export const StateTypeEnum = z.nativeEnum(StateType); export type ToggleSubscription = z.infer; const sortDirectionValue = z.enum(['asc', 'desc']); -const sortPropEnum = z.enum(['title', 'state', 'priority', 'project', 'activity', 'owner', 'updatedAt', 'createdAt']); +const sortGoalsPropEnum = z.enum([ + 'title', + 'state', + 'priority', + 'project', + 'activity', + 'owner', + 'updatedAt', + 'createdAt', +]); -export const sortablePropertiesArraySchema = z +const sortProjectsPropEnum = z.enum(['title', 'owner', 'updatedAt', 'createdAt', 'stargizers', 'watchers', 'goals']); + +export const sortableGoalsPropertiesArraySchema = z .array( z.object({ - key: sortPropEnum, + key: sortGoalsPropEnum, dir: sortDirectionValue, }), ) .optional(); +export const sortableProjectsPropertiesArraySchema = z + .array( + z.object({ + key: sortProjectsPropEnum, + dir: sortDirectionValue, + }), + ) + .optional(); + +export type SortableProjectsPropertiesArray = z.infer; + export const queryWithFiltersSchema = z.object({ priority: z.array(z.string()).optional(), state: z.array(z.string()).optional(), @@ -33,7 +55,7 @@ export const queryWithFiltersSchema = z.object({ participant: z.array(z.string()).optional(), project: z.array(z.string()).optional(), partnershipProject: z.array(z.string()).optional(), - sort: sortablePropertiesArraySchema, + sort: sortableGoalsPropertiesArraySchema, query: z.string().optional(), starred: z.boolean().optional(), watching: z.boolean().optional(), diff --git a/trpc/queries/projectV2.ts b/trpc/queries/projectV2.ts index 93a1f8337..e347a281b 100644 --- a/trpc/queries/projectV2.ts +++ b/trpc/queries/projectV2.ts @@ -1,9 +1,10 @@ -import { sql } from 'kysely'; +import { AnyColumnWithTable, Expression, OrderByExpression, sql } from 'kysely'; import { jsonBuildObject } from 'kysely/helpers/postgres'; +import { OrderByDirection } from 'kysely/dist/cjs/parser/order-by-parser'; import { db } from '../connection/kysely'; -import { Role } from '../../generated/kysely/types'; -import { QueryWithFilters } from '../../src/schema/common'; +import { DB, Role } from '../../generated/kysely/types'; +import { QueryWithFilters, SortableProjectsPropertiesArray } from '../../src/schema/common'; import { decodeUrlDateRange, getDateString } from '../../src/utils/dateTime'; import { mapSortParamsToTableColumns } from './goalV2'; @@ -223,9 +224,53 @@ export const getUserProjectsQuery = ({ .orderBy('Project.updatedAt desc'); }; +const mapProjectsSortParamsToTableColumns = ( + sort: SortableProjectsPropertiesArray = [], +): Array> => { + if (!sort.length) { + return ['Project.updatedAt desc']; + } + + const mapToTableColumn: Record< + NonNullable[number]['key'], + AnyColumnWithTable | Record> + > = { + title: 'Project.title', + updatedAt: 'Project.updatedAt', + createdAt: 'Project.createdAt', + stargizers: { + asc: sql`(select count("A") from "_projectStargizers" where "B" = "Project".id) asc`, + desc: sql`(select count("A") from "_projectStargizers" where "B" = "Project".id) desc`, + }, + watchers: { + asc: sql`(select count("A") from "_projectStargizers" where "B" = "Project".id) asc`, + desc: sql`(select count("A") from "_projectStargizers" where "B" = "Project".id) desc`, + }, + owner: { + asc: sql`(select name from "User" where "User"."activityId" = "Project"."activityId") asc`, + desc: sql`(select name from "User" where "User"."activityId" = "Project"."activityId") desc`, + }, + goals: { + asc: sql`(select count(*) from "Goal" where "Goal"."projectId" = "Project".id) asc`, + desc: sql`(select count(*) from "Goal" where "Goal"."projectId" = "Project".id) desc`, + }, + }; + + return sort.map>(({ key, dir }) => { + const rule = mapToTableColumn[key]; + + if (typeof rule === 'string') { + return `${rule} ${dir}`; + } + + return rule[dir]; + }); +}; + interface GetUserDashboardProjectsParams extends GetUserProjectsQueryParams { in?: Array<{ id: string }>; goalsQuery?: QueryWithFilters; + projectsSort?: SortableProjectsPropertiesArray; limit?: number; offset?: number; } @@ -452,7 +497,7 @@ export const getUserDashboardProjects = (params: GetUserDashboardProjectsParams) ]), ) .groupBy('Project.id') - .orderBy('Project.updatedAt desc') + .orderBy(mapProjectsSortParamsToTableColumns(params.projectsSort)) .$if(!!params.goalsQuery?.hideEmptyProjects, (qb) => qb.having(({ fn }) => fn.count('goal.id'), '>', 0)) .limit(params.limit || 5) .offset(params.offset || 0); @@ -530,6 +575,7 @@ interface GetAllProjectsQueryParams { limit: number; cursor: number; ids?: string[]; + projectsSort?: SortableProjectsPropertiesArray; } export const getAllProjectsQuery = ({ @@ -539,6 +585,7 @@ export const getAllProjectsQuery = ({ ids = [], limit, cursor, + projectsSort, }: GetAllProjectsQueryParams) => { return db .selectFrom(({ selectFrom }) => @@ -590,7 +637,7 @@ export const getAllProjectsQuery = ({ ) .limit(limit) .offset(cursor) - .orderBy(['Project.updatedAt desc', 'Project.id asc']) + .orderBy(mapProjectsSortParamsToTableColumns(projectsSort)) .groupBy(['Project.id']) .as('projects'), ) diff --git a/trpc/router/projectV2.ts b/trpc/router/projectV2.ts index e4f365c03..a0ec4a0fd 100644 --- a/trpc/router/projectV2.ts +++ b/trpc/router/projectV2.ts @@ -15,7 +15,7 @@ import { getAllProjectsQuery, getChildrenProjectQuery, } from '../queries/projectV2'; -import { queryWithFiltersSchema } from '../../src/schema/common'; +import { queryWithFiltersSchema, sortableProjectsPropertiesArraySchema } from '../../src/schema/common'; import { Project, User, @@ -169,10 +169,11 @@ export const project = router({ limit: z.number().optional(), goalsQuery: queryWithFiltersSchema.optional(), cursor: z.number().optional(), + projectsSort: sortableProjectsPropertiesArraySchema.optional(), }), ) .query(async ({ ctx, input }) => { - const { limit = 5, cursor: offset = 0, goalsQuery } = input; + const { limit = 5, cursor: offset = 0, goalsQuery, projectsSort } = input; const { session: { user }, } = ctx; @@ -180,6 +181,7 @@ export const project = router({ const dashboardProjects = await getUserDashboardProjects({ ...user, goalsQuery, + projectsSort, limit: limit + 1, offset, }) @@ -229,12 +231,13 @@ export const project = router({ z.object({ limit: z.number().optional(), goalsQuery: queryWithFiltersSchema.optional(), + projectsSort: sortableProjectsPropertiesArraySchema.optional(), firstLevel: z.boolean(), cursor: z.number().optional(), }), ) .query(async ({ input, ctx }) => { - const { limit = 20, cursor = 0, goalsQuery, firstLevel: _ = true } = input; + const { limit = 20, cursor = 0, goalsQuery, projectsSort, firstLevel: _ = true } = input; const projects = await getAllProjectsQuery({ ...ctx.session.user, @@ -242,6 +245,7 @@ export const project = router({ limit: limit + 1, cursor, ids: goalsQuery?.project, + projectsSort, }) .$castTo & Pick>() .execute();