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/email/template-components/template-access-auth-2fa.tsx b/packages/email/template-components/template-access-auth-2fa.tsx index 7873a1735..a7918ad4b 100644 --- a/packages/email/template-components/template-access-auth-2fa.tsx +++ b/packages/email/template-components/template-access-auth-2fa.tsx @@ -1,6 +1,7 @@ import { Plural, Trans } from '@lingui/react/macro'; import { Heading, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; export type TemplateAccessAuth2FAProps = { documentTitle: string; @@ -18,13 +19,9 @@ export const TemplateAccessAuth2FA = ({ expiresInMinutes, assetBaseUrl = 'http://localhost:3002', }: TemplateAccessAuth2FAProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return (
- Document + Document
diff --git a/packages/email/template-components/template-branding-logo.tsx b/packages/email/template-components/template-branding-logo.tsx index c7e99684e..6b560aed2 100644 --- a/packages/email/template-components/template-branding-logo.tsx +++ b/packages/email/template-components/template-branding-logo.tsx @@ -1,5 +1,6 @@ import { Img, Link } from '../components'; import { useBranding } from '../providers/branding'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { getSafeBrandingUrl } from '../utils/branding-url'; export type TemplateBrandingLogoProps = { @@ -20,7 +21,7 @@ export const TemplateBrandingLogo = ({ assetBaseUrl, className = 'mb-4 h-6' }: T const hasCustomBrandingLogo = branding.brandingEnabled && Boolean(branding.brandingLogo); if (!hasCustomBrandingLogo) { - const documensoLogoUrl = new URL('/static/logo.png', assetBaseUrl).toString(); + const documensoLogoUrl = getEmailAssetUrl(assetBaseUrl, 'static/logo.png'); return Documenso Logo; } diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx index ec453f315..20b7000e1 100644 --- a/packages/email/template-components/template-document-completed.tsx +++ b/packages/email/template-components/template-document-completed.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/react/macro'; import { Button, Column, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentCompletedProps { @@ -16,10 +17,6 @@ export const TemplateDocumentCompleted = ({ assetBaseUrl, customBody, }: TemplateDocumentCompletedProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( <> @@ -29,7 +26,7 @@ export const TemplateDocumentCompleted = ({ @@ -51,7 +48,11 @@ export const TemplateDocumentCompleted = ({ className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" href={downloadLink} > - + Download
diff --git a/packages/email/template-components/template-document-image.tsx b/packages/email/template-components/template-document-image.tsx index ed45512a5..88c06282c 100644 --- a/packages/email/template-components/template-document-image.tsx +++ b/packages/email/template-components/template-document-image.tsx @@ -1,4 +1,5 @@ import { Column, Img, Row, Section } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; export interface TemplateDocumentImageProps { assetBaseUrl: string; @@ -6,17 +7,13 @@ export interface TemplateDocumentImageProps { } export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return (
- Documenso + Documenso diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx index c44ae5c81..5cc435e28 100644 --- a/packages/email/template-components/template-document-pending.tsx +++ b/packages/email/template-components/template-document-pending.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/react/macro'; import { Column, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentPendingProps { @@ -9,10 +10,6 @@ export interface TemplateDocumentPendingProps { } export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: TemplateDocumentPendingProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( <> @@ -21,7 +18,11 @@ export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: Template
- + Waiting for others diff --git a/packages/email/template-components/template-document-recipient-signed.tsx b/packages/email/template-components/template-document-recipient-signed.tsx index 42ebe0e10..9077091fb 100644 --- a/packages/email/template-components/template-document-recipient-signed.tsx +++ b/packages/email/template-components/template-document-recipient-signed.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/react/macro'; import { Column, Img, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentRecipientSignedProps { @@ -16,10 +17,6 @@ export const TemplateDocumentRecipientSigned = ({ recipientEmail, assetBaseUrl, }: TemplateDocumentRecipientSignedProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - const recipientReference = recipientName || recipientEmail; return ( @@ -31,7 +28,7 @@ export const TemplateDocumentRecipientSigned = ({ diff --git a/packages/email/template-components/template-document-self-signed.tsx b/packages/email/template-components/template-document-self-signed.tsx index 852843577..729a3b156 100644 --- a/packages/email/template-components/template-document-self-signed.tsx +++ b/packages/email/template-components/template-document-self-signed.tsx @@ -2,6 +2,7 @@ import { env } from '@documenso/lib/utils/env'; import { Trans } from '@lingui/react/macro'; import { Button, Column, Img, Link, Section, Text } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; import { TemplateDocumentImage } from './template-document-image'; export interface TemplateDocumentSelfSignedProps { @@ -14,10 +15,6 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ const signUpUrl = `${NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`; - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( <> @@ -27,7 +24,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ @@ -56,7 +53,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ className="mr-4 rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" > @@ -67,7 +64,11 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" href="https://documenso.com/pricing" > - + View plans
diff --git a/packages/email/template-components/template-image.tsx b/packages/email/template-components/template-image.tsx index b16bcfe60..198c55aec 100644 --- a/packages/email/template-components/template-image.tsx +++ b/packages/email/template-components/template-image.tsx @@ -1,4 +1,5 @@ import { Img } from '../components'; +import { getEmailAssetUrl } from '../utils/asset-url'; export interface TemplateImageProps { assetBaseUrl: string; @@ -7,11 +8,7 @@ export interface TemplateImageProps { } export const TemplateImage = ({ assetBaseUrl, className, staticAsset }: TemplateImageProps) => { - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - - return ; + return ; }; export default TemplateImage; diff --git a/packages/email/templates/admin-user-created.tsx b/packages/email/templates/admin-user-created.tsx index ccc92020b..000b4d3cb 100644 --- a/packages/email/templates/admin-user-created.tsx +++ b/packages/email/templates/admin-user-created.tsx @@ -5,6 +5,7 @@ import { Body, Container, Head, Html, Img, Preview, Section } from '../component import type { TemplateAdminUserCreatedProps } from '../template-components/template-admin-user-created'; import { TemplateAdminUserCreated } from '../template-components/template-admin-user-created'; import { TemplateFooter } from '../template-components/template-footer'; +import { getEmailAssetUrl } from '../utils/asset-url'; export const AdminUserCreatedTemplate = ({ resetPasswordLink, @@ -14,10 +15,6 @@ export const AdminUserCreatedTemplate = ({ const previewText = msg`Set your password for Documenso`; - const getAssetUrl = (path: string) => { - return new URL(path, assetBaseUrl).toString(); - }; - return ( @@ -27,7 +24,7 @@ export const AdminUserCreatedTemplate = ({
- Documenso Logo + Documenso Logo
diff --git a/packages/email/utils/asset-url.ts b/packages/email/utils/asset-url.ts new file mode 100644 index 000000000..f6143de19 --- /dev/null +++ b/packages/email/utils/asset-url.ts @@ -0,0 +1,17 @@ +/** + * Resolve a static email asset path against the asset base URL. + * + * The base is normalised to end with a trailing slash and the path is + * normalised to have no leading slash, so a sub-path in the base URL + * (e.g. "/ESign") is preserved. Passing a root-absolute path straight to + * `new URL()` would otherwise replace the base pathname entirely. + * + * `getEmailAssetUrl('https://host/ESign', 'static/logo.png')` -> `https://host/ESign/static/logo.png` + * `getEmailAssetUrl('https://host/ESign/', '/static/logo.png')` -> `https://host/ESign/static/logo.png` + */ +export const getEmailAssetUrl = (assetBaseUrl: string, path: string): string => { + const base = assetBaseUrl.endsWith('/') ? assetBaseUrl : `${assetBaseUrl}/`; + const relativePath = path.startsWith('/') ? path.slice(1) : path; + + return new URL(relativePath, base).toString(); +}; 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 }); });