diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e82e501f..1341dae53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6539,6 +6539,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: diff --git a/scripts/check-db.js b/scripts/check-db.js index 8c806cd8f..aaa128af5 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -67,7 +67,8 @@ async function checkDatabaseVersion() { async function applyMigration() { if (!process.env.SKIP_DB_MIGRATION) { - console.log(execSync('prisma migrate deploy').toString()); + const directUrl = process.env.DIRECT_DATABASE_URL || process.env.DATABASE_URL; + console.log(execSync('prisma migrate deploy', { env: { ...process.env, DATABASE_URL: directUrl } }).toString()); success('Database is up to date.'); } diff --git a/src/app/(main)/admin/teams/AdminTeamsTable.tsx b/src/app/(main)/admin/teams/AdminTeamsTable.tsx index 8213b6d04..1f7cca5be 100644 --- a/src/app/(main)/admin/teams/AdminTeamsTable.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from import Link from '@/components/common/Link'; import { useState } from 'react'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; @@ -21,7 +22,11 @@ export function AdminTeamsTable({ return ( <> - + } + width="1fr" + > {(row: any) => {row.name}} @@ -41,7 +46,11 @@ export function AdminTeamsTable({ ); }} - + } + width="160px" + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/admin/users/UsersTable.tsx b/src/app/(main)/admin/users/UsersTable.tsx index fafe0e139..299cf3da3 100644 --- a/src/app/(main)/admin/users/UsersTable.tsx +++ b/src/app/(main)/admin/users/UsersTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, Icon, MenuItem, Modal, Row, Text } from '@umami/ import Link from '@/components/common/Link'; import { useState } from 'react'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; @@ -22,10 +23,14 @@ export function UsersTable({ return ( <> - + } + width="2fr" + > {(row: any) => {row.username}} - + }> {(row: any) => t(labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown) } @@ -33,7 +38,10 @@ export function UsersTable({ {(row: any) => row._count.websites} - + } + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx index 751dc7583..edf048120 100644 --- a/src/app/(main)/admin/websites/AdminWebsitesTable.tsx +++ b/src/app/(main)/admin/websites/AdminWebsitesTable.tsx @@ -3,6 +3,7 @@ import Link from '@/components/common/Link'; import { useState } from 'react'; import { WebsiteDeleteForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { Edit, Trash, Users } from '@/components/icons'; import { MenuButton } from '@/components/input/MenuButton'; @@ -14,14 +15,14 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) { return ( <> - + }> {(row: any) => ( {row.name} )} - + }> {(row: any) => {row.domain}} @@ -45,7 +46,11 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) { ); }} - + } + width="180px" + > {(row: any) => } diff --git a/src/app/(main)/boards/BoardsTable.tsx b/src/app/(main)/boards/BoardsTable.tsx index b21cbee00..0465012cc 100644 --- a/src/app/(main)/boards/BoardsTable.tsx +++ b/src/app/(main)/boards/BoardsTable.tsx @@ -1,6 +1,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; import Link from '@/components/common/Link'; import { DateDistance } from '@/components/common/DateDistance'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation } from '@/components/hooks'; import { BoardDeleteButton } from './BoardDeleteButton'; import { BoardDesignButton } from './BoardDesignButton'; @@ -12,16 +13,23 @@ export function BoardsTable(props: DataTableProps) { return ( - + }> {({ id, name }: any) => { return {name}; }} - - + } + /> + }> {({ type }: any) => type ? type.charAt(0).toUpperCase() + type.slice(1) : ''} - + } + width="200px" + > {(row: any) => } diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx index bf9b5ef7f..1758272e9 100644 --- a/src/app/(main)/links/LinksTable.tsx +++ b/src/app/(main)/links/LinksTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-ze import Link from '@/components/common/Link'; import { DateDistance } from '@/components/common/DateDistance'; import { ExternalLink } from '@/components/common/ExternalLink'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation, useSlug } from '@/components/hooks'; import { LinkDeleteButton } from './LinkDeleteButton'; import { LinkEditButton } from './LinkEditButton'; @@ -17,23 +18,27 @@ export function LinksTable({ showActions, ...props }: LinksTableProps) { return ( - + }> {({ id, name }: any) => { return {name}; }} - + }> {({ slug }: any) => { const url = getSlugUrl(slug); return {url}; }} - + }> {({ url }: any) => { return {url}; }} - + } + width="200px" + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx index e9bc76ad8..362bffa78 100644 --- a/src/app/(main)/pixels/PixelsTable.tsx +++ b/src/app/(main)/pixels/PixelsTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-ze import Link from '@/components/common/Link'; import { DateDistance } from '@/components/common/DateDistance'; import { ExternalLink } from '@/components/common/ExternalLink'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation, useSlug } from '@/components/hooks'; import { PixelDeleteButton } from './PixelDeleteButton'; import { PixelEditButton } from './PixelEditButton'; @@ -17,12 +18,12 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) { return ( - + }> {({ id, name }: any) => { return {name}; }} - + }> {({ slug }: any) => { const url = getSlugUrl(slug); return ( @@ -32,7 +33,10 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) { ); }} - + } + > {(row: any) => } {showActions && ( diff --git a/src/app/(main)/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx index 66c7355ac..77515b7b2 100644 --- a/src/app/(main)/teams/TeamsTable.tsx +++ b/src/app/(main)/teams/TeamsTable.tsx @@ -1,5 +1,6 @@ import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen'; import type { ReactNode } from 'react'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { ROLES } from '@/lib/constants'; @@ -12,7 +13,7 @@ export function TeamsTable({ renderLink, ...props }: TeamsTableProps) { return ( - + }> {renderLink} diff --git a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx index 86d846bb2..e57a10775 100644 --- a/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx +++ b/src/app/(main)/teams/[teamId]/TeamWebsitesTable.tsx @@ -2,6 +2,7 @@ import { DataColumn, DataTable, Row } from '@umami/react-zen'; import Link from '@/components/common/Link'; import { TeamMemberEditButton } from '@/app/(main)/teams/[teamId]/TeamMemberEditButton'; import { TeamMemberRemoveButton } from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages } from '@/components/hooks'; import { ROLES } from '@/lib/constants'; @@ -18,10 +19,10 @@ export function TeamWebsitesTable({ return ( - + }> {(row: any) => {row.name}} - + } /> {(row: any) => row?.createUser?.username} diff --git a/src/app/(main)/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx index 714e1c66c..bfa13e4ae 100644 --- a/src/app/(main)/websites/WebsitesTable.tsx +++ b/src/app/(main)/websites/WebsitesTable.tsx @@ -1,6 +1,7 @@ import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen'; import type { ReactNode } from 'react'; import { LinkButton } from '@/components/common/LinkButton'; +import { SortableLabel } from '@/components/common/SortableLabel'; import { useMessages, useNavigation } from '@/components/hooks'; import { SquarePen } from '@/components/icons'; @@ -17,10 +18,10 @@ export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTab return ( - + }> {renderLink} - + } /> {showActions && ( {(row: any) => { diff --git a/src/app/api/admin/teams/route.ts b/src/app/api/admin/teams/route.ts index ceb16ab11..35c3f5363 100644 --- a/src/app/api/admin/teams/route.ts +++ b/src/app/api/admin/teams/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewAllTeams } from '@/permissions'; import { getTeams } from '@/queries/prisma/team'; @@ -9,6 +9,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 2e5226157..0bf94d511 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewUsers } from '@/permissions'; import { getUsers } from '@/queries/prisma/user'; @@ -9,6 +9,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts index 09b2ef98d..0150e21fd 100644 --- a/src/app/api/admin/websites/route.ts +++ b/src/app/api/admin/websites/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { ROLES } from '@/lib/constants'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewAllWebsites } from '@/permissions'; import { getWebsites } from '@/queries/prisma/website'; @@ -10,6 +10,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/boards/route.ts b/src/app/api/boards/route.ts index bb0710370..655486182 100644 --- a/src/app/api/boards/route.ts +++ b/src/app/api/boards/route.ts @@ -3,7 +3,7 @@ import { BOARD_TYPES, normalizeBoardType } from '@/lib/boards'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createBoard, getUserBoards } from '@/queries/prisma'; @@ -11,6 +11,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/links/route.ts b/src/app/api/links/route.ts index a639888bd..7c9172546 100644 --- a/src/app/api/links/route.ts +++ b/src/app/api/links/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createLink, getUserLinks } from '@/queries/prisma'; @@ -10,6 +10,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/me/teams/route.ts b/src/app/api/me/teams/route.ts index 555bf3003..12c6de43f 100644 --- a/src/app/api/me/teams/route.ts +++ b/src/app/api/me/teams/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { getUserTeams } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts index 0880f84e1..809c6e502 100644 --- a/src/app/api/me/websites/route.ts +++ b/src/app/api/me/websites/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams, + ...sortingParams, includeTeams: z.string().optional(), }); diff --git a/src/app/api/pixels/route.ts b/src/app/api/pixels/route.ts index 8baae4f3e..453ede247 100644 --- a/src/app/api/pixels/route.ts +++ b/src/app/api/pixels/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createPixel, getUserPixels } from '@/queries/prisma'; @@ -10,6 +10,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/boards/route.ts b/src/app/api/teams/[teamId]/boards/route.ts index 2e60b77e2..b4e645e49 100644 --- a/src/app/api/teams/[teamId]/boards/route.ts +++ b/src/app/api/teams/[teamId]/boards/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamBoards } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/links/route.ts b/src/app/api/teams/[teamId]/links/route.ts index 41e139b33..4794902a1 100644 --- a/src/app/api/teams/[teamId]/links/route.ts +++ b/src/app/api/teams/[teamId]/links/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamLinks } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/pixels/route.ts b/src/app/api/teams/[teamId]/pixels/route.ts index daac2040d..2ce204f7b 100644 --- a/src/app/api/teams/[teamId]/pixels/route.ts +++ b/src/app/api/teams/[teamId]/pixels/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamPixels } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts index 05c6d8045..5d010799b 100644 --- a/src/app/api/teams/[teamId]/websites/route.ts +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canViewTeam } from '@/permissions'; import { getTeamWebsites } from '@/queries/prisma'; @@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, }); const { teamId } = await params; const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index 2e2424a2f..6c79be7fc 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -5,13 +5,14 @@ import { fetchAccount } from '@/lib/load'; import redis from '@/lib/redis'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { canCreateTeam } from '@/permissions'; import { createTeam, getUserTeams } from '@/queries/prisma'; export async function GET(request: Request) { const schema = z.object({ ...pagingParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts index 7a834a3f1..ff8b88290 100644 --- a/src/app/api/users/[userId]/teams/route.ts +++ b/src/app/api/users/[userId]/teams/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams } from '@/lib/schema'; +import { pagingParams, sortingParams } from '@/lib/schema'; import { getUserTeams } from '@/queries/prisma'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ ...pagingParams, + ...sortingParams, }); const { auth, query, error } = await parseRequest(request, schema); diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts index 4587354fb..c706dae9e 100644 --- a/src/app/api/users/[userId]/websites/route.ts +++ b/src/app/api/users/[userId]/websites/route.ts @@ -1,13 +1,14 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma/website'; export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, includeTeams: z.string().optional(), }); diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 288ddfe30..fd4553fb8 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto'; import { fetchAccount } from '@/lib/load'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { pagingParams, searchParams } from '@/lib/schema'; +import { pagingParams, searchParams, sortingParams } from '@/lib/schema'; import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions'; import { createShare, createWebsite, getWebsiteCount } from '@/queries/prisma'; import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma/website'; @@ -15,6 +15,7 @@ export async function GET(request: Request) { const schema = z.object({ ...pagingParams, ...searchParams, + ...sortingParams, includeTeams: z.string().optional(), }); diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx index 8d18321cf..9a601a129 100644 --- a/src/components/common/DataGrid.tsx +++ b/src/components/common/DataGrid.tsx @@ -41,7 +41,7 @@ export function DataGrid({ const { data, error, isLoading, isFetching } = query; const { router, updateParams, query: queryParams } = useNavigation(); const [search, setSearch] = useState(queryParams?.search || data?.search || ''); - const showPager = allowPaging && data && (data.count > data.pageSize || data.isCapped); + const showPager = allowPaging && data && data.count > 0; const { isMobile } = useMobile(); const displayMode = isMobile ? 'cards' : undefined; diff --git a/src/components/common/Pager.tsx b/src/components/common/Pager.tsx index ff22dc70e..f3d72fb47 100644 --- a/src/components/common/Pager.tsx +++ b/src/components/common/Pager.tsx @@ -11,11 +11,18 @@ export interface PagerProps { className?: string; } -export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerProps) { +export function Pager({ + page, + pageSize, + count, + isCapped, + onPageChange, +}: PagerProps) { const { t, labels } = useMessages(); const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0; const lastPage = page === maxPage; const firstPage = page === 1; + const showNavigation = maxPage > 1 || isCapped; if (count === 0 || !maxPage) { return null; @@ -29,34 +36,40 @@ export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerPr } }; - if (maxPage === 1 && !isCapped) { - return null; - } - const displayCount = isCapped ? `10,000+` : (+count).toLocaleString(); return ( - - {t(labels.numberOfRecords, { x: displayCount })} - - - {t(labels.pageOf, { - current: page.toLocaleString(), - total: maxPage.toLocaleString(), - })} - - - - - + + {t(labels.numberOfRecords, { x: displayCount })} + + {showNavigation && ( + <> + + {t(labels.pageOf, { + current: page.toLocaleString(), + total: maxPage.toLocaleString(), + })} + + + + + + + )} ); diff --git a/src/components/common/SortableLabel.tsx b/src/components/common/SortableLabel.tsx new file mode 100644 index 000000000..c45550c8c --- /dev/null +++ b/src/components/common/SortableLabel.tsx @@ -0,0 +1,94 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { Icon } from '@umami/react-zen'; +import { ChevronDown, ChevronUp } from '@/components/icons'; +import { useNavigation } from '@/components/hooks'; + +type SortDirection = 'asc' | 'desc'; + +export interface SortableLabelProps { + label: ReactNode; + sortKey: string; + defaultDirection?: SortDirection; +} + +export function SortableLabel({ + label, + sortKey, + defaultDirection = 'asc', +}: SortableLabelProps) { + const { router, query, updateParams } = useNavigation(); + const isActive = query.orderBy === sortKey; + const isDescending = query.sortDescending === 'true'; + const direction = isActive ? (isDescending ? 'desc' : 'asc') : undefined; + const activeColor = 'var(--text-primary)'; + + const getNextDirection = (): SortDirection => { + if (!isActive) { + return defaultDirection; + } + + return direction === 'desc' ? 'asc' : 'desc'; + }; + + const handleSort = () => { + const nextDirection = getNextDirection(); + + router.push( + updateParams({ + orderBy: sortKey, + sortDescending: nextDirection === 'desc' ? 'true' : undefined, + page: 1, + }), + ); + }; + + return ( + + ); +} diff --git a/src/components/hooks/queries/useUserTeamsQuery.ts b/src/components/hooks/queries/useUserTeamsQuery.ts index 82f65496b..825e8def3 100644 --- a/src/components/hooks/queries/useUserTeamsQuery.ts +++ b/src/components/hooks/queries/useUserTeamsQuery.ts @@ -1,14 +1,15 @@ import { useApi } from '../useApi'; import { useModified } from '../useModified'; +import { usePagedQuery } from '../usePagedQuery'; export function useUserTeamsQuery(userId: string) { - const { get, useQuery } = useApi(); + const { get } = useApi(); const { modified } = useModified(`teams`); - return useQuery({ + return usePagedQuery({ queryKey: ['teams', { userId, modified }], - queryFn: () => { - return get(`/users/${userId}/teams`); + queryFn: params => { + return get(`/users/${userId}/teams`, params); }, enabled: !!userId, }); diff --git a/src/components/hooks/usePageParameters.ts b/src/components/hooks/usePageParameters.ts index 42cf3911c..0644e6900 100644 --- a/src/components/hooks/usePageParameters.ts +++ b/src/components/hooks/usePageParameters.ts @@ -3,7 +3,7 @@ import { useNavigation } from './useNavigation'; export function usePageParameters() { const { - query: { page, pageSize, search }, + query: { page, pageSize, search, orderBy, sortDescending }, } = useNavigation(); return useMemo(() => { @@ -11,6 +11,8 @@ export function usePageParameters() { page, pageSize, search, + orderBy, + sortDescending, }; - }, [page, pageSize, search]); + }, [orderBy, page, pageSize, search, sortDescending]); } diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts index c818de643..73a1ead99 100644 --- a/src/components/hooks/usePagedQuery.ts +++ b/src/components/hooks/usePagedQuery.ts @@ -15,13 +15,13 @@ export function usePagedQuery({ queryFn: (params?: object) => Promise> | PageResult; }): UseQueryResult, TError> { const { - query: { page, search }, + query: { page, search, orderBy, sortDescending }, } = useNavigation(); const { useQuery } = useApi(); return useQuery, TError>({ - queryKey: [...queryKey, page, search] as const, - queryFn: () => queryFn({ page, search }), + queryKey: [...queryKey, page, search, orderBy, sortDescending] as const, + queryFn: () => queryFn({ page, search, orderBy, sortDescending }), ...options, }); } diff --git a/src/components/input/DialogButton.tsx b/src/components/input/DialogButton.tsx index 6038d733b..98faf9d5d 100644 --- a/src/components/input/DialogButton.tsx +++ b/src/components/input/DialogButton.tsx @@ -42,7 +42,7 @@ export function DialogButton({ height, minWidth, minHeight, - maxHeight: 'calc(100dvh - 40px)', + maxHeight: 'min(80dvh, calc(100dvh - 40px))', overflowY: 'auto', padding: '32px', }; diff --git a/src/lib/__tests__/data.test.ts b/src/lib/__tests__/data.test.ts new file mode 100644 index 000000000..61882f6c3 --- /dev/null +++ b/src/lib/__tests__/data.test.ts @@ -0,0 +1,246 @@ +import { DATA_TYPE } from '../constants'; +import { + flattenJSON, + createKeyValue, + isValidDateValue, + getDataType, + getStringValue, + objectToArray, + type KeyValueData, +} from '../data'; + +describe('isValidDateValue', () => { + test.each([ + ['2024-01-15T10:30:00Z', true], + ['2024-01-15T10:30:00.123Z', true], + ['2024-01-15T10:30:00+02:00', true], + ['not-a-date', false], + ['2024/01/15', false], + ['', false], + ])('validates datetime strings correctly (%s → %s)', (input, expected) => { + expect(isValidDateValue(input)).toBe(expected); + }); + + test('returns false for non-string values', () => { + expect(isValidDateValue(123 as any)).toBe(false); + expect(isValidDateValue(null as any)).toBe(false); + expect(isValidDateValue(undefined as any)).toBe(false); + }); +}); + +describe('getDataType', () => { + test.each([ + ['string', 'string'], + [123, 'number'], + [true, 'boolean'], + [null, 'object'], + [[], 'object'], + [{}, 'object'], + ['2024-01-15T10:30:00Z', 'date'], + ])('detects type correctly (%s → %s)', (input, expected) => { + expect(getDataType(input)).toBe(expected); + }); +}); + +describe('createKeyValue', () => { + test('handles string values', () => { + const result = createKeyValue('name', 'test'); + expect(result).toEqual({ + key: 'name', + value: 'test', + dataType: DATA_TYPE.string, + }); + }); + + test('handles number values', () => { + const result = createKeyValue('count', 42); + expect(result).toEqual({ + key: 'count', + value: 42, + dataType: DATA_TYPE.number, + }); + }); + + test('handles boolean values and converts to string', () => { + expect(createKeyValue('active', true)).toEqual({ + key: 'active', + value: 'true', + dataType: DATA_TYPE.boolean, + }); + expect(createKeyValue('active', false)).toEqual({ + key: 'active', + value: 'false', + dataType: DATA_TYPE.boolean, + }); + }); + + test('handles date strings', () => { + const dateStr = '2024-01-15T10:30:00Z'; + const result = createKeyValue('timestamp', dateStr); + expect(result).toEqual({ + key: 'timestamp', + value: dateStr, + dataType: DATA_TYPE.date, + }); + }); + + test('handles arrays and converts to JSON string', () => { + const arr = [1, 2, 3]; + const result = createKeyValue('items', arr); + expect(result).toEqual({ + key: 'items', + value: '[1,2,3]', + dataType: DATA_TYPE.array, + }); + }); + + test('handles null values', () => { + const result = createKeyValue('nullable', null); + expect(result.dataType).toBe(DATA_TYPE.array); + expect(result.value).toBe('null'); + }); +}); + +describe('getStringValue', () => { + test('formats number values with 4 decimal places', () => { + expect(getStringValue('42', DATA_TYPE.number)).toBe('42.0000'); + expect(getStringValue('3.14159', DATA_TYPE.number)).toBe('3.1416'); + }); + + test('converts date values to ISO string', () => { + const result = getStringValue('2024-01-15T10:30:00', DATA_TYPE.date); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + + test('returns string values as-is for other types', () => { + expect(getStringValue('test', DATA_TYPE.string)).toBe('test'); + expect(getStringValue('true', DATA_TYPE.boolean)).toBe('true'); + }); +}); + +describe('objectToArray', () => { + test('converts object values to array', () => { + const obj = { a: 1, b: 2, c: 3 }; + const result = objectToArray(obj); + expect(result).toEqual([1, 2, 3]); + }); + + test('returns empty array for empty object', () => { + expect(objectToArray({})).toEqual([]); + }); +}); + +describe('flattenJSON', () => { + test('flattens simple object', () => { + const input = { + name: 'test', + count: 42, + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(2); + expect(result).toContainEqual(expect.objectContaining({ key: 'name', value: 'test' })); + expect(result).toContainEqual(expect.objectContaining({ key: 'count', value: 42 })); + }); + + test('flattens nested object with dot notation', () => { + const input = { + user: { + name: 'John', + age: 30, + }, + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(2); + expect(result).toContainEqual(expect.objectContaining({ key: 'user.name', value: 'John' })); + expect(result).toContainEqual(expect.objectContaining({ key: 'user.age', value: 30 })); + }); + + test('flattens deeply nested object', () => { + const input = { + level1: { + level2: { + level3: { + value: 'deep', + }, + }, + }, + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('level1.level2.level3.value'); + expect(result[0].value).toBe('deep'); + }); + + test('treats arrays as leaf values (converts to JSON string)', () => { + const input = { + tags: ['a', 'b', 'c'], + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('tags'); + expect(result[0].value).toBe('["a","b","c"]'); + expect(result[0].dataType).toBe(DATA_TYPE.array); + }); + + test('treats date strings as leaf values', () => { + const input = { + createdAt: '2024-01-15T10:30:00Z', + }; + + const result = flattenJSON(input); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('createdAt'); + expect(result[0].dataType).toBe(DATA_TYPE.date); + }); + + test('handles mixed nested and flat structure', () => { + const input = { + id: '123', + metadata: { + timestamp: '2024-01-15T10:30:00Z', + source: 'web', + details: { + referrer: 'google', + }, + }, + }; + + const result = flattenJSON(input); + const keys = result.map(r => r.key); + + expect(result).toHaveLength(4); + expect(keys).toContain('id'); + expect(keys).toContain('metadata.timestamp'); + expect(keys).toContain('metadata.source'); + expect(keys).toContain('metadata.details.referrer'); + }); + + test('returns empty array for empty object', () => { + expect(flattenJSON({})).toEqual([]); + }); + + test('converts boolean values to strings', () => { + const input = { + isActive: true, + isAdmin: false, + }; + + const result = flattenJSON(input); + + expect(result).toContainEqual( + expect.objectContaining({ key: 'isActive', value: 'true', dataType: DATA_TYPE.boolean }), + ); + expect(result).toContainEqual( + expect.objectContaining({ key: 'isAdmin', value: 'false', dataType: DATA_TYPE.boolean }), + ); + }); +}); diff --git a/src/lib/data.ts b/src/lib/data.ts index fe69edf04..07eae52a5 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -1,27 +1,26 @@ import { DATA_TYPE, DATETIME_REGEX } from './constants'; import type { DynamicDataType } from './types'; -export function flattenJSON( - eventData: Record, - keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [], - parentKey = '', -): { key: string; value: any; dataType: DynamicDataType }[] { - return Object.keys(eventData).reduce( - (acc, key) => { - const value = eventData[key]; - const type = typeof eventData[key]; +export interface KeyValueData { + key: string; + value: any; + dataType: DynamicDataType; +} - // nested object - if (value && type === 'object' && !Array.isArray(value) && !isValidDateValue(value)) { - flattenJSON(value, acc.keyValues, getKeyName(key, parentKey)); - } else { - createKey(getKeyName(key, parentKey), value, acc); +export function flattenJSON(eventData: Record): KeyValueData[] { + function flatten(obj: Record, parentKey: string): KeyValueData[] { + return Object.entries(obj).flatMap(([key, value]) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (value && typeof value === 'object' && !Array.isArray(value) && !isValidDateValue(value)) { + return flatten(value, fullKey); } - return acc; - }, - { keyValues, parentKey }, - ).keyValues; + return [createKeyValue(fullKey, value)]; + }); + } + + return flatten(eventData, ''); } export function isValidDateValue(value: string) { @@ -50,10 +49,10 @@ export function getStringValue(value: string, dataType: number) { return value; } -function createKey(key: string, value: string, acc: { keyValues: any[]; parentKey: string }) { +export function createKeyValue(key: string, value: any): KeyValueData { const type = getDataType(value); - - let dataType = null; + let dataType: DynamicDataType; + let processedValue = value; switch (type) { case 'number': @@ -64,29 +63,21 @@ function createKey(key: string, value: string, acc: { keyValues: any[]; parentKe break; case 'boolean': dataType = DATA_TYPE.boolean; - value = value ? 'true' : 'false'; + processedValue = value ? 'true' : 'false'; break; case 'date': dataType = DATA_TYPE.date; break; case 'object': dataType = DATA_TYPE.array; - value = JSON.stringify(value); + processedValue = JSON.stringify(value); break; default: dataType = DATA_TYPE.string; break; } - acc.keyValues.push({ key, value, dataType }); -} - -function getKeyName(key: string, parentKey: string) { - if (!parentKey) { - return key; - } - - return `${parentKey}.${key}`; + return { key, value: processedValue, dataType }; } export function objectToArray(obj: object) { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 113b24cf8..71f8bb22b 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -82,6 +82,16 @@ export const pagingParams = { export const sortingParams = { orderBy: z.string().optional(), + sortDescending: z + .enum(['true', 'false']) + .optional() + .transform(value => { + if (value === undefined) { + return undefined; + } + + return value === 'true'; + }), }; export const userRoleParam = z.enum(['admin', 'user', 'view-only']); diff --git a/src/lib/sort.ts b/src/lib/sort.ts new file mode 100644 index 000000000..881c2371a --- /dev/null +++ b/src/lib/sort.ts @@ -0,0 +1,27 @@ +import type { QueryFilters } from './types'; + +export function sanitizeSortFilters( + filters: QueryFilters = {}, + allowedFields: T, + defaults: Partial> = {}, +): QueryFilters { + const { orderBy, sortDescending, ...rest } = filters; + const fallbackOrderBy = defaults.orderBy; + const fallbackSortDescending = defaults.sortDescending; + const isAllowed = orderBy ? allowedFields.includes(orderBy as T[number]) : false; + + return { + ...rest, + ...(isAllowed + ? { + orderBy, + sortDescending, + } + : { + ...(fallbackOrderBy && { orderBy: fallbackOrderBy }), + ...(fallbackSortDescending !== undefined && { + sortDescending: fallbackSortDescending, + }), + }), + }; +} diff --git a/src/queries/prisma/board.ts b/src/queries/prisma/board.ts index 862ec36dd..d433850d7 100644 --- a/src/queries/prisma/board.ts +++ b/src/queries/prisma/board.ts @@ -1,8 +1,11 @@ import type { Prisma } from '@/generated/prisma/client'; import { BOARD_TYPES } from '@/lib/boards'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const BOARD_SORT_FIELDS = ['name', 'description', 'type', 'createdAt'] as const; + export async function findBoard(criteria: Prisma.BoardFindUniqueArgs) { return prisma.client.board.findUnique(criteria); } @@ -16,7 +19,8 @@ export async function getBoard(boardId: string) { } export async function getBoards(criteria: Prisma.BoardFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, BOARD_SORT_FIELDS); + const { search } = sortFilters; const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.BoardWhereInput = { @@ -24,7 +28,7 @@ export async function getBoards(criteria: Prisma.BoardFindManyArgs, filters: Que ...getSearchParameters(search, [{ name: 'contains' }, { description: 'contains' }]), }; - return pagedQuery('board', { ...criteria, where }, filters); + return pagedQuery('board', { ...criteria, where }, sortFilters); } export async function getUserBoards(userId: string, filters?: QueryFilters) { diff --git a/src/queries/prisma/link.ts b/src/queries/prisma/link.ts index 9b971dec4..c59adc025 100644 --- a/src/queries/prisma/link.ts +++ b/src/queries/prisma/link.ts @@ -1,7 +1,10 @@ import type { Prisma } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const LINK_SORT_FIELDS = ['name', 'slug', 'url', 'createdAt'] as const; + export async function findLink(criteria: Prisma.LinkFindUniqueArgs) { return prisma.client.link.findUnique(criteria); } @@ -15,7 +18,8 @@ export async function getLink(linkId: string) { } export async function getLinks(criteria: Prisma.LinkFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, LINK_SORT_FIELDS); + const { search } = sortFilters; const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.LinkWhereInput = { @@ -27,7 +31,7 @@ export async function getLinks(criteria: Prisma.LinkFindManyArgs, filters: Query ]), }; - return pagedQuery('link', { ...criteria, where }, filters); + return pagedQuery('link', { ...criteria, where }, sortFilters); } export async function getUserLinks(userId: string, filters?: QueryFilters) { diff --git a/src/queries/prisma/pixel.ts b/src/queries/prisma/pixel.ts index 4c9e132d5..2ea9c6f28 100644 --- a/src/queries/prisma/pixel.ts +++ b/src/queries/prisma/pixel.ts @@ -1,7 +1,10 @@ import type { Prisma } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const PIXEL_SORT_FIELDS = ['name', 'slug', 'createdAt'] as const; + export async function findPixel(criteria: Prisma.PixelFindUniqueArgs) { return prisma.client.pixel.findUnique(criteria); } @@ -15,14 +18,15 @@ export async function getPixel(pixelId: string) { } export async function getPixels(criteria: Prisma.PixelFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, PIXEL_SORT_FIELDS); + const { search } = sortFilters; const where: Prisma.PixelWhereInput = { ...criteria.where, ...prisma.getSearchParameters(search, [{ name: 'contains' }, { slug: 'contains' }]), }; - return prisma.pagedQuery('pixel', { ...criteria, where }, filters); + return prisma.pagedQuery('pixel', { ...criteria, where }, sortFilters); } export async function getUserPixels(userId: string, filters?: QueryFilters) { diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index de938df35..7315921e1 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -2,10 +2,13 @@ import { Prisma, type Team } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import { uuid } from '@/lib/crypto'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { PageResult, QueryFilters } from '@/lib/types'; import TeamFindManyArgs = Prisma.TeamFindManyArgs; +const TEAM_SORT_FIELDS = ['name', 'createdAt'] as const; + export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise { return prisma.client.team.findUnique(criteria); } @@ -29,7 +32,8 @@ export async function getTeams( filters: QueryFilters, ): Promise> { const { getSearchParameters } = prisma; - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, TEAM_SORT_FIELDS); + const { search } = sortFilters; const where: Prisma.TeamWhereInput = { ...criteria.where, @@ -42,7 +46,7 @@ export async function getTeams( ...criteria, where, }, - filters, + sortFilters, ); } diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 467ea1e02..0d9b1c4cf 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -2,10 +2,13 @@ import { Prisma } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import { getRandomChars } from '@/lib/generate'; import prisma from '@/lib/prisma'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters, Role } from '@/lib/types'; import UserFindManyArgs = Prisma.UserFindManyArgs; +const USER_SORT_FIELDS = ['username', 'role', 'createdAt'] as const; + export interface GetUserOptions { includePassword?: boolean; showDeleted?: boolean; @@ -46,7 +49,11 @@ export async function getUserByUsername(username: string, options: GetUserOption } export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters = {}) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, USER_SORT_FIELDS, { + orderBy: 'createdAt', + sortDescending: true, + }); + const { search } = sortFilters; const where: Prisma.UserWhereInput = { ...criteria.where, @@ -60,11 +67,7 @@ export async function getUsers(criteria: UserFindManyArgs, filters: QueryFilters ...criteria, where, }, - { - orderBy: 'createdAt', - sortDescending: true, - ...filters, - }, + sortFilters, ); } diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index ae7ea6c96..254e27e15 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -2,8 +2,11 @@ import type { Prisma, Website } from '@/generated/prisma/client'; import { ROLES } from '@/lib/constants'; import prisma from '@/lib/prisma'; import redis from '@/lib/redis'; +import { sanitizeSortFilters } from '@/lib/sort'; import type { QueryFilters } from '@/lib/types'; +const WEBSITE_SORT_FIELDS = ['name', 'domain', 'createdAt'] as const; + export async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs) { return prisma.client.website.findUnique(criteria); } @@ -23,7 +26,8 @@ export async function getWebsite(websiteId: string) { } export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) { - const { search } = filters; + const sortFilters = sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS); + const { search } = sortFilters; const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.WebsiteWhereInput = { @@ -37,7 +41,7 @@ export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: deletedAt: null, }; - const websites = await pagedQuery('website', { ...criteria, where }, filters); + const websites = await pagedQuery('website', { ...criteria, where }, sortFilters); return attachShareIdToWebsites(websites); } @@ -62,10 +66,7 @@ export async function getAllUserWebsitesIncludingTeamAccess(userId: string, filt ], }, }, - { - orderBy: 'name', - ...filters, - }, + sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS, { orderBy: 'name' }), ); } @@ -84,10 +85,7 @@ export async function getUserWebsites(userId: string, filters?: QueryFilters) { }, }, }, - { - orderBy: 'name', - ...filters, - }, + sanitizeSortFilters(filters, WEBSITE_SORT_FIELDS, { orderBy: 'name' }), ); } diff --git a/src/queries/sql/getValues.ts b/src/queries/sql/getValues.ts index 908be4126..bd462dc0d 100644 --- a/src/queries/sql/getValues.ts +++ b/src/queries/sql/getValues.ts @@ -96,10 +96,6 @@ async function clickhouseQuery(websiteId: string, column: string, filters: Query excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`; } - if (search) { - searchQuery = `and positionCaseInsensitive(${column}, {search:String}) > 0`; - } - if (search) { if (decodeURIComponent(search).includes(',')) { searchQuery = `AND (${decodeURIComponent(search)