diff --git a/apps/remix/app/routes/_authenticated+/admin+/claims.tsx b/apps/remix/app/routes/_authenticated+/admin+/claims.tsx index 3a4c13011..1bf479f0e 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/claims.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/claims.tsx @@ -1,9 +1,7 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { LicenseClient } from '@documenso/lib/server-only/license/license-client'; import { Input } from '@documenso/ui/primitives/input'; import { useLingui } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { ClaimCreateDialog } from '~/components/dialogs/claim-create-dialog'; import { SettingsHeader } from '~/components/general/settings-header'; @@ -24,32 +22,10 @@ export default function Claims({ loaderData }: Route.ComponentProps) { const { t } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); - const { pathname } = useLocation(); - - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); return (
@@ -59,8 +35,8 @@ export default function Claims({ loaderData }: Route.ComponentProps) {
setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search by claim ID or name`} className="mb-4" /> diff --git a/apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx b/apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx index ecbcfbe65..4e88fc1a5 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx @@ -1,8 +1,6 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { Input } from '@documenso/ui/primitives/input'; import { useLingui } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { EmailTransportCreateDialog } from '~/components/dialogs/email-transport-create-dialog'; import { SettingsHeader } from '~/components/general/settings-header'; @@ -11,32 +9,10 @@ import { AdminEmailTransportsTable } from '~/components/tables/admin-email-trans export default function AdminEmailTransportsPage() { const { t } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); - const { pathname } = useLocation(); - - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); return (
@@ -46,8 +22,8 @@ export default function AdminEmailTransportsPage() {
setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search by name or from address`} className="mb-4" /> diff --git a/apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx b/apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx index 258ff3f99..5c2d70ee3 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx @@ -1,4 +1,3 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { currentMonthlyPeriod } from '@documenso/lib/universal/monthly-period'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; @@ -6,8 +5,9 @@ import { Input } from '@documenso/ui/primitives/input'; import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; import { Trans, useLingui } from '@lingui/react/macro'; -import { useEffect, useMemo, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsInteger, parseAsString, useQueryStates } from 'nuqs'; +import { useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router'; import { SettingsHeader } from '~/components/general/settings-header'; import { @@ -52,14 +52,17 @@ export default function OrganisationStats() { const { t } = useLingui(); const [searchParams, setSearchParams] = useSearchParams(); - const { pathname } = useLocation(); - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); + const [{ query: searchQuery }, setSearchFilters] = useQueryStates( + { + query: parseAsString.withDefault(''), + page: parseAsInteger, + }, + { shallow: false, limitUrlUpdates: debounce(500) }, + ); const [displayMode, setDisplayMode] = useState('usage'); - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - const periodOptions = useMemo(() => generatePeriodOptions(), []); const selectedPeriod = searchParams?.get('period') ?? currentMonthlyPeriod(); @@ -71,29 +74,12 @@ export default function OrganisationStats() { const claimOptions = claimsData?.data ?? []; - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - if ((searchParams?.get('query') || '') !== debouncedSearchQuery) { - params.delete('page'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const onSearchQueryChange = (value: string) => { + void setSearchFilters({ + query: value || null, + page: null, + }); + }; const onPeriodChange = (value: string) => { const params = new URLSearchParams(searchParams?.toString()); @@ -128,8 +114,8 @@ export default function OrganisationStats() {
setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => onSearchQueryChange(e.target.value)} placeholder={t`Search by organisation name, URL or ID`} className="flex-1" /> diff --git a/apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx b/apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx index 58847e872..7724668d8 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx @@ -1,8 +1,6 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { Input } from '@documenso/ui/primitives/input'; import { useLingui } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { SettingsHeader } from '~/components/general/settings-header'; import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table'; @@ -10,32 +8,10 @@ import { AdminOrganisationsTable } from '~/components/tables/admin-organisations export default function Organisations() { const { t } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); - const { pathname } = useLocation(); - - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); return (
@@ -43,8 +19,8 @@ export default function Organisations() {
setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search by organisation ID, name, customer ID or owner email`} className="mb-4" /> diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.members.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.members.tsx index cd89680e7..375a48773 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.members.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.members.tsx @@ -1,10 +1,9 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { Input } from '@documenso/ui/primitives/input'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { Link, useLocation, useSearchParams } from 'react-router'; import { OrganisationMemberInviteDialog } from '~/components/dialogs/organisation-member-invite-dialog'; @@ -15,35 +14,16 @@ import { OrganisationMembersDataTable } from '~/components/tables/organisation-m export default function TeamsSettingsMembersPage() { const { _ } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const { pathname } = useLocation(); - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); const currentTab = searchParams?.get('tab') === 'invites' ? 'invites' : 'members'; - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); - return (
setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={_(msg`Search`)} /> diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.teams.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.teams.tsx index e2408b4a8..8f54555b7 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.teams.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.teams.tsx @@ -1,8 +1,6 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { Input } from '@documenso/ui/primitives/input'; import { useLingui } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { TeamCreateDialog } from '~/components/dialogs/team-create-dialog'; import { SettingsHeader } from '~/components/general/settings-header'; @@ -11,27 +9,10 @@ import { OrganisationTeamsTable } from '~/components/tables/organisation-teams-t export default function OrganisationSettingsTeamsPage() { const { t } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); - const { pathname } = useLocation(); - - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); return (
@@ -40,8 +21,8 @@ export default function OrganisationSettingsTeamsPage() { setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search`} className="mb-4" /> diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.groups.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.groups.tsx index cae095e1b..e02eda14e 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.groups.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.groups.tsx @@ -1,11 +1,9 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { trpc } from '@documenso/trpc/react'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { Input } from '@documenso/ui/primitives/input'; import { useLingui } from '@lingui/react/macro'; import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client'; -import { useEffect, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { TeamGroupCreateDialog } from '~/components/dialogs/team-group-create-dialog'; import { SettingsHeader } from '~/components/general/settings-header'; @@ -16,34 +14,12 @@ import { useCurrentTeam } from '~/providers/team'; export default function TeamsSettingsGroupsPage() { const { t } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); - - const { pathname } = useLocation(); const team = useCurrentTeam(); - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); const everyoneGroupQuery = trpc.team.group.find.useQuery({ teamId: team.id, @@ -61,8 +37,8 @@ export default function TeamsSettingsGroupsPage() { setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search`} className="mb-4" /> diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.members.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.members.tsx index 6a2da2d53..12a2b3a6f 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.members.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.members.tsx @@ -1,8 +1,6 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { Input } from '@documenso/ui/primitives/input'; import { useLingui } from '@lingui/react/macro'; -import { useEffect, useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; import { TeamMemberCreateDialog } from '~/components/dialogs/team-member-create-dialog'; import { SettingsHeader } from '~/components/general/settings-header'; @@ -11,32 +9,10 @@ import { TeamMembersTable } from '~/components/tables/team-members-table'; export default function TeamsSettingsMembersPage() { const { t } = useLingui(); - const [searchParams, setSearchParams] = useSearchParams(); - const { pathname } = useLocation(); - - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); - - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); return (
@@ -45,8 +21,8 @@ export default function TeamsSettingsMembersPage() { setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search`} className="mb-4" /> diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id._index.tsx index faff66cf6..d334df802 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id._index.tsx @@ -1,4 +1,3 @@ -import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params'; @@ -20,7 +19,8 @@ import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { WebhookCallStatus, WebhookTriggerEvents } from '@prisma/client'; import { CheckCircle2Icon, ChevronRightIcon, PencilIcon, TerminalIcon, XCircleIcon } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { debounce, parseAsString, useQueryState } from 'nuqs'; +import { useMemo } from 'react'; import { Link, useLocation, useNavigate, useSearchParams } from 'react-router'; import { z } from 'zod'; @@ -51,13 +51,14 @@ export default function WebhookPage({ params }: Route.ComponentProps) { const { toast } = useToast(); const { pathname } = useLocation(); - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const updateSearchParams = useUpdateSearchParams(); const team = useCurrentTeam(); - const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? ''); - - const debouncedSearchQuery = useDebouncedValue(searchQuery, 500); + const [searchQuery, setSearchQuery] = useQueryState( + 'query', + parseAsString.withDefault('').withOptions({ shallow: false, limitUrlUpdates: debounce(500) }), + ); const parsedSearchParams = WebhookSearchParamsSchema.parse(Object.fromEntries(searchParams ?? [])); @@ -81,26 +82,6 @@ export default function WebhookPage({ params }: Route.ComponentProps) { query: parsedSearchParams.query, }); - /** - * Handle debouncing the search query. - */ - useEffect(() => { - const params = new URLSearchParams(searchParams?.toString()); - - params.set('query', debouncedSearchQuery); - - if (debouncedSearchQuery === '') { - params.delete('query'); - } - - // If nothing to change then do nothing. - if (params.toString() === searchParams?.toString()) { - return; - } - - setSearchParams(params); - }, [debouncedSearchQuery, pathname, searchParams]); - const onPaginationChange = (page: number, perPage: number) => { updateSearchParams({ page, @@ -252,8 +233,8 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
setSearchQuery(e.target.value)} + value={searchQuery} + onChange={(e) => void setSearchQuery(e.target.value || null)} placeholder={t`Search by ID`} /> diff --git a/packages/trpc/server/team-router/find-teams.ts b/packages/trpc/server/team-router/find-teams.ts index e4466d96c..825637687 100644 --- a/packages/trpc/server/team-router/find-teams.ts +++ b/packages/trpc/server/team-router/find-teams.ts @@ -8,7 +8,7 @@ export const findTeamsRoute = authenticatedProcedure .input(ZFindTeamsRequestSchema) .output(ZFindTeamsResponseSchema) .query(async ({ ctx, input }) => { - const { organisationId } = input; + const { organisationId, query, page, perPage } = input; const { user } = ctx; ctx.logger.info({ @@ -17,5 +17,5 @@ export const findTeamsRoute = authenticatedProcedure }, }); - return findTeams({ userId: user.id, organisationId }); + return findTeams({ userId: user.id, organisationId, query, page, perPage }); });