diff --git a/src/components/common/DataTable.js b/src/components/common/DataTable.js index cb739344..2662fa2c 100644 --- a/src/components/common/DataTable.js +++ b/src/components/common/DataTable.js @@ -25,13 +25,13 @@ export function DataTable({ const { page, pageSize, count } = pageInfo || {}; const noResults = Boolean(query && data?.length === 0); - const handleChange = () => { - onChange?.({ query, page }); + const handleChange = value => { + onChange?.({ query: value, page }); }; const handleSearch = value => { setQuery(value); - handleChange(); + handleChange(value); }; const handlePageChange = page => { diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js index 70bbbd92..4761ad0a 100644 --- a/src/components/pages/settings/websites/WebsitesList.js +++ b/src/components/pages/settings/websites/WebsitesList.js @@ -7,7 +7,7 @@ import useMessages from 'components/hooks/useMessages'; import useUser from 'components/hooks/useUser'; import { ROLES } from 'lib/constants'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; export function WebsitesList({ showTeam, @@ -20,16 +20,20 @@ export function WebsitesList({ const { user } = useUser(); const [params, setParams] = useState({}); const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['websites', includeTeams, onlyTeams], - () => - get(`/users/${user?.id}/websites`, { + const count = useRef(0); + const q = useQuery( + ['websites', includeTeams, onlyTeams, params], + () => { + count.current += 1; + return get(`/users/${user?.id}/websites`, { includeTeams, onlyTeams, ...params, - }), + }); + }, { enabled: !!user }, ); + const { data, refetch, isLoading, error } = q; const { showToast } = useToasts(); const handleChange = params => { @@ -60,10 +64,10 @@ export function WebsitesList({ ); return ( - + {showHeader && {addButton}} {showTable && ( - + {showTeam && ( diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 00000000..739128b3 --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export const dateRange = { + startAt: yup.number().integer().required(), + endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), +}; + +export const pageInfo = { + query: yup.string(), + page: yup.number().integer().positive(), + pageSize: yup.number().integer().positive().max(200), + orderBy: yup.string(), +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3685753e..58e6aa9e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -54,11 +54,11 @@ export interface ReportSearchFilter extends SearchFilter } export interface SearchFilter { - filter?: string; - filterType?: T; - pageSize: number; - page: number; + query?: string; + page?: number; + pageSize?: number; orderBy?: string; + data?: T; } export interface FilterResult { diff --git a/src/lib/yup.ts b/src/lib/yup.ts deleted file mode 100644 index a9d21028..00000000 --- a/src/lib/yup.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as yup from 'yup'; - -export function getDateRangeValidation() { - return { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(), - }; -} - -// ex: /funnel|insights|retention/i -export function getFilterValidation(matchRegex) { - return { - filter: yup.string(), - filterType: yup.string().matches(matchRegex), - pageSize: yup.number().integer().positive().max(200), - page: yup.number().integer().positive(), - orderBy: yup.string(), - }; -} diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts index d394ef07..131cb262 100644 --- a/src/pages/api/me/teams.ts +++ b/src/pages/api/me/teams.ts @@ -1,6 +1,6 @@ import { useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userTeams from 'pages/api/users/[id]/teams'; @@ -12,7 +12,7 @@ export interface MyTeamsRequestQuery extends SearchFilter const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Owner/i), + ...pageInfo, }), }; diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts index d4a803a0..749af316 100644 --- a/src/pages/api/me/websites.ts +++ b/src/pages/api/me/websites.ts @@ -1,6 +1,6 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed } from 'next-basics'; import userWebsites from 'pages/api/users/[id]/websites'; @@ -12,7 +12,7 @@ export interface MyWebsitesRequestQuery extends SearchFilter const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Owner/i), + ...pageInfo, }), POST: yup.object().shape({ name: yup.string().max(50).required(), @@ -39,12 +39,11 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const { page, filter, pageSize } = req.query; + const { page, query } = req.query; const results = await getTeamsByUserId(userId, { page, - filter, - pageSize: +pageSize || undefined, + query, }); return ok(res, results); diff --git a/src/pages/api/users/[id]/teams.ts b/src/pages/api/users/[id]/teams.ts index 72b99b86..34a31a0e 100644 --- a/src/pages/api/users/[id]/teams.ts +++ b/src/pages/api/users/[id]/teams.ts @@ -1,10 +1,11 @@ +import * as yup from 'yup'; import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, TeamSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getTeamsByUserId } from 'queries'; -import * as yup from 'yup'; + export interface UserTeamsRequestQuery extends SearchFilter { id: string; } @@ -18,7 +19,7 @@ export interface UserTeamsRequestBody { const schema = { GET: yup.object().shape({ id: yup.string().uuid().required(), - ...getFilterValidation('/All|Name|Owner/i'), + ...pageInfo, }), }; @@ -40,12 +41,12 @@ export default async ( return unauthorized(res); } - const { page, filter, pageSize } = req.query; + const { page, query, pageSize } = req.query; const teams = await getTeamsByUserId(userId, { + query, page, - filter, - pageSize: +pageSize || undefined, + pageSize, }); return ok(res, teams); diff --git a/src/pages/api/users/[id]/websites.ts b/src/pages/api/users/[id]/websites.ts index ab7d88ef..cc264e7d 100644 --- a/src/pages/api/users/[id]/websites.ts +++ b/src/pages/api/users/[id]/websites.ts @@ -1,6 +1,6 @@ import { useAuth, useCors, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, SearchFilter, WebsiteSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getWebsitesByUserId } from 'queries'; @@ -17,7 +17,7 @@ const schema = { id: yup.string().uuid().required(), includeTeams: yup.boolean(), onlyTeams: yup.boolean(), - ...getFilterValidation(/All|Name|Domain/i), + ...pageInfo, }), }; @@ -32,7 +32,7 @@ export default async ( await useValidate(req, res); const { user } = req.auth; - const { id: userId, page, filter, pageSize, includeTeams, onlyTeams } = req.query; + const { id: userId, page, pageSize, query, includeTeams, onlyTeams } = req.query; if (req.method === 'GET') { if (!user.isAdmin && user.id !== userId) { @@ -40,8 +40,8 @@ export default async ( } const websites = await getWebsitesByUserId(userId, { + query, page, - filter, pageSize: +pageSize || undefined, includeTeams, onlyTeams, diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index 991986e8..d37add2f 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -3,7 +3,7 @@ import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth, useValidate } from 'lib/middleware'; import { NextApiRequestQueryBody, Role, SearchFilter, User, UserSearchFilterType } from 'lib/types'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUserByUsername, getUsers } from 'queries'; @@ -19,7 +19,7 @@ export interface UsersRequestBody { import * as yup from 'yup'; const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Username/i), + ...pageInfo, }), POST: yup.object().shape({ username: yup.string().max(255).required(), diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts index d6009caf..a90f8e46 100644 --- a/src/pages/api/websites/index.ts +++ b/src/pages/api/websites/index.ts @@ -7,7 +7,7 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite } from 'queries'; import userWebsites from 'pages/api/users/[id]/websites'; import * as yup from 'yup'; -import { getFilterValidation } from 'lib/yup'; +import { pageInfo } from 'lib/schema'; export interface WebsitesRequestQuery extends SearchFilter {} @@ -19,7 +19,7 @@ export interface WebsitesRequestBody { const schema = { GET: yup.object().shape({ - ...getFilterValidation(/All|Name|Domain/i), + ...pageInfo, }), POST: yup.object().shape({ name: yup.string().max(100).required(), diff --git a/src/queries/admin/team.ts b/src/queries/admin/team.ts index cf731ad4..9947b9a3 100644 --- a/src/queries/admin/team.ts +++ b/src/queries/admin/team.ts @@ -1,5 +1,5 @@ import { Prisma, Team } from '@prisma/client'; -import { ROLES, TEAM_FILTER_TYPES } from 'lib/constants'; +import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import prisma from 'lib/prisma'; import { FilterResult, TeamSearchFilter } from 'lib/types'; @@ -82,10 +82,10 @@ export async function deleteTeam( } export async function getTeams( - TeamSearchFilter: TeamSearchFilter, + filters: TeamSearchFilter, options?: { include?: Prisma.TeamInclude }, ): Promise> { - const { userId, filter, filterType = TEAM_FILTER_TYPES.all } = TeamSearchFilter; + const { userId, query } = filters; const mode = prisma.getSearchMode(); const where: Prisma.TeamWhereInput = { @@ -94,29 +94,24 @@ export async function getTeams( some: { userId }, }, }), - ...(filter && { + ...(query && { AND: { OR: [ { - ...((filterType === TEAM_FILTER_TYPES.all || filterType === TEAM_FILTER_TYPES.name) && { - name: { startsWith: filter, ...mode }, - }), + name: { startsWith: query, ...mode }, }, { - ...((filterType === TEAM_FILTER_TYPES.all || - filterType === TEAM_FILTER_TYPES['user:username']) && { - teamUser: { - some: { - role: ROLES.teamOwner, - user: { - username: { - startsWith: filter, - ...mode, - }, + teamUser: { + some: { + role: ROLES.teamOwner, + user: { + username: { + startsWith: query, + ...mode, }, }, }, - }), + }, }, ], }, @@ -125,7 +120,7 @@ export async function getTeams( const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'name', - ...TeamSearchFilter, + ...filters, }); const teams = await prisma.client.team.findMany({ diff --git a/src/queries/admin/website.ts b/src/queries/admin/website.ts index 6417ade6..f4444b53 100644 --- a/src/queries/admin/website.ts +++ b/src/queries/admin/website.ts @@ -1,6 +1,6 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; -import { ROLES, WEBSITE_FILTER_TYPES } from 'lib/constants'; +import { ROLES } from 'lib/constants'; import prisma from 'lib/prisma'; import { FilterResult, WebsiteSearchFilter } from 'lib/types'; @@ -19,17 +19,10 @@ export async function getWebsiteByShareId(shareId: string) { } export async function getWebsites( - WebsiteSearchFilter: WebsiteSearchFilter, + filters: WebsiteSearchFilter, options?: { include?: Prisma.WebsiteInclude }, ): Promise> { - const { - userId, - teamId, - includeTeams, - onlyTeams, - filter, - filterType = WEBSITE_FILTER_TYPES.all, - } = WebsiteSearchFilter; + const { userId, teamId, includeTeams, onlyTeams, query } = filters; const mode = prisma.getSearchMode(); const where: Prisma.WebsiteWhereInput = { @@ -76,27 +69,23 @@ export async function getWebsites( ], }, { - OR: [ - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.name) && { - name: { startsWith: filter, ...mode }, - }), - }, - { - ...((filterType === WEBSITE_FILTER_TYPES.all || - filterType === WEBSITE_FILTER_TYPES.domain) && { - domain: { startsWith: filter, ...mode }, - }), - }, - ], + OR: query + ? [ + { + name: { startsWith: query, ...mode }, + }, + { + domain: { startsWith: query, ...mode }, + }, + ] + : [], }, ], }; const [pageFilters, getParameters] = prisma.getPageFilters({ orderBy: 'name', - ...WebsiteSearchFilter, + ...filters, }); const websites = await prisma.client.website.findMany({ @@ -115,10 +104,10 @@ export async function getWebsites( export async function getWebsitesByUserId( userId: string, - filter?: WebsiteSearchFilter, + filters?: WebsiteSearchFilter, ): Promise> { return getWebsites( - { userId, ...filter }, + { userId, ...filters }, { include: { teamWebsite: { @@ -143,12 +132,12 @@ export async function getWebsitesByUserId( export async function getWebsitesByTeamId( teamId: string, - filter?: WebsiteSearchFilter, + filters?: WebsiteSearchFilter, ): Promise> { return getWebsites( { teamId, - ...filter, + ...filters, includeTeams: true, }, {