mirror of
https://github.com/documenso/documenso.git
synced 2026-09-29 16:24:30 +10:00
Merge branch 'main' into feat/org-team-allow-document-rejection-setting
This commit is contained in:
@@ -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 (
|
||||
<div>
|
||||
@@ -59,8 +35,8 @@ export default function Claims({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
<div className="mt-4">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -46,8 +22,8 @@ export default function AdminEmailTransportsPage() {
|
||||
|
||||
<div className="mt-4">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
|
||||
@@ -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<OrganisationStatsDisplayMode>('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() {
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 sm:flex-row">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchQueryChange(e.target.value)}
|
||||
placeholder={t`Search by organisation name, URL or ID`}
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -43,8 +19,8 @@ export default function Organisations() {
|
||||
|
||||
<div className="mt-4">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
@@ -57,8 +37,8 @@ export default function TeamsSettingsMembersPage() {
|
||||
<div>
|
||||
<div className="my-4 flex flex-row items-center justify-between space-x-4">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => void setSearchQuery(e.target.value || null)}
|
||||
placeholder={_(msg`Search`)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -40,8 +21,8 @@ export default function OrganisationSettingsTeamsPage() {
|
||||
</SettingsHeader>
|
||||
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => void setSearchQuery(e.target.value || null)}
|
||||
placeholder={t`Search`}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
@@ -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() {
|
||||
</SettingsHeader>
|
||||
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => void setSearchQuery(e.target.value || null)}
|
||||
placeholder={t`Search`}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
@@ -45,8 +21,8 @@ export default function TeamsSettingsMembersPage() {
|
||||
</SettingsHeader>
|
||||
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => void setSearchQuery(e.target.value || null)}
|
||||
placeholder={t`Search`}
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
<div className="mt-4">
|
||||
<div className="mb-4 flex flex-row items-center justify-between gap-x-4">
|
||||
<Input
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => void setSearchQuery(e.target.value || null)}
|
||||
placeholder={t`Search by ID`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<Img src={getAssetUrl('/static/document.png')} alt="Document" className="mx-auto h-12 w-12" />
|
||||
<Img src={getEmailAssetUrl(assetBaseUrl, 'static/document.png')} alt="Document" className="mx-auto h-12 w-12" />
|
||||
|
||||
<Section className="mt-8">
|
||||
<Heading className="text-center font-semibold text-foreground text-lg">
|
||||
|
||||
@@ -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 <Img src={documensoLogoUrl} alt="Documenso Logo" className={className} />;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
@@ -29,7 +26,7 @@ export const TemplateDocumentCompleted = ({
|
||||
<Column align="center">
|
||||
<Text className="font-semibold text-base text-foreground">
|
||||
<Img
|
||||
src={getAssetUrl('/static/completed.png')}
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/completed.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
@@ -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}
|
||||
>
|
||||
<Img src={getAssetUrl('/static/download.png')} className="mr-2 mb-0.5 inline h-5 w-5 align-middle" alt="" />
|
||||
<Img
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/download.png')}
|
||||
className="mr-2 mb-0.5 inline h-5 w-5 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@@ -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 (
|
||||
<Section className={className}>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<Column>
|
||||
<Img className="mx-auto h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
<Img className="mx-auto h-42" src={getEmailAssetUrl(assetBaseUrl, 'static/document.png')} alt="Documenso" />
|
||||
</Column>
|
||||
|
||||
<Column />
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
@@ -21,7 +18,11 @@ export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: Template
|
||||
<Section className="mb-4">
|
||||
<Column align="center">
|
||||
<Text className="font-semibold text-base text-foreground">
|
||||
<Img src={getAssetUrl('/static/clock.png')} className="-mt-0.5 mr-2 inline h-7 w-7 align-middle" alt="" />
|
||||
<Img
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/clock.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
<Trans>Waiting for others</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
|
||||
@@ -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 = ({
|
||||
<Column align="center">
|
||||
<Text className="font-semibold text-base text-foreground">
|
||||
<Img
|
||||
src={getAssetUrl('/static/completed.png')}
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/completed.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
@@ -27,7 +24,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ
|
||||
<Column align="center">
|
||||
<Text className="font-semibold text-base text-foreground">
|
||||
<Img
|
||||
src={getAssetUrl('/static/completed.png')}
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/completed.png')}
|
||||
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
@@ -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"
|
||||
>
|
||||
<Img
|
||||
src={getAssetUrl('/static/user-plus.png')}
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/user-plus.png')}
|
||||
className="mr-2 mb-0.5 inline h-5 w-5 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
@@ -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"
|
||||
>
|
||||
<Img src={getAssetUrl('/static/review.png')} className="mr-2 mb-0.5 inline h-5 w-5 align-middle" alt="" />
|
||||
<Img
|
||||
src={getEmailAssetUrl(assetBaseUrl, 'static/review.png')}
|
||||
className="mr-2 mb-0.5 inline h-5 w-5 align-middle"
|
||||
alt=""
|
||||
/>
|
||||
<Trans>View plans</Trans>
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@@ -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 <Img className={className} src={getAssetUrl(`/static/${staticAsset}`)} alt="" />;
|
||||
return <Img className={className} src={getEmailAssetUrl(assetBaseUrl, `static/${staticAsset}`)} alt="" />;
|
||||
};
|
||||
|
||||
export default TemplateImage;
|
||||
|
||||
@@ -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 (
|
||||
<Html>
|
||||
<Head />
|
||||
@@ -27,7 +24,7 @@ export const AdminUserCreatedTemplate = ({
|
||||
<Section>
|
||||
<Container className="mx-auto mt-8 mb-2 max-w-xl rounded-lg border border-border border-solid p-4 backdrop-blur-sm">
|
||||
<Section>
|
||||
<Img src={getAssetUrl('/static/logo.png')} alt="Documenso Logo" className="mb-4 h-6" />
|
||||
<Img src={getEmailAssetUrl(assetBaseUrl, 'static/logo.png')} alt="Documenso Logo" className="mb-4 h-6" />
|
||||
|
||||
<TemplateAdminUserCreated resetPasswordLink={resetPasswordLink} assetBaseUrl={assetBaseUrl} />
|
||||
</Section>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user