chore: merge main, resolve biome formatting conflicts

Merge origin/main into feat/external-2fa-codes. Resolve formatting
conflicts caused by biome rollout; preserve both feature streams:
PR's external 2FA token + signing-session 2FA proof additions plus
main's RateLimit/RecipientExpired/signingReminders/date-auto-insert.

In complete-document-with-token.ts, drop the duplicate early
field-fetching block introduced when main moved that logic later
with date auto-insert support; keep the EXTERNAL_TWO_FACTOR_AUTH
check using derivedRecipientActionAuth.
This commit is contained in:
ephraimduncan
2026-05-12 11:46:11 +00:00
parent 9194884fbe
commit 138d663c25
1959 changed files with 93488 additions and 47038 deletions
@@ -1,10 +1,3 @@
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { EditIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { TLicenseClaim } from '@documenso/lib/types/license';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -24,6 +17,10 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { EditIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router';
import { ClaimDeleteDialog } from '../dialogs/claim-delete-dialog';
import { ClaimUpdateDialog } from '../dialogs/claim-update-dialog';
@@ -68,20 +65,13 @@ export const AdminClaimsTable = ({ licenseFlags }: AdminClaimsTableProps) => {
accessorKey: 'id',
maxSize: 50,
cell: ({ row }) => (
<CopyTextButton
value={row.original.id}
onCopySuccess={() => toast({ title: t`ID copied to clipboard` })}
/>
<CopyTextButton value={row.original.id} onCopySuccess={() => toast({ title: t`ID copied to clipboard` })} />
),
},
{
header: t`Name`,
accessorKey: 'name',
cell: ({ row }) => (
<Link to={`/admin/organisations?query=claim:${row.original.id}`}>
{row.original.name}
</Link>
),
cell: ({ row }) => <Link to={`/admin/organisations?query=claim:${row.original.id}`}>{row.original.name}</Link>,
},
{
header: t`Allowed teams`,
@@ -97,16 +87,14 @@ export const AdminClaimsTable = ({ licenseFlags }: AdminClaimsTableProps) => {
{
header: t`Feature Flags`,
cell: ({ row }) => {
const flags = Object.values(SUBSCRIPTION_CLAIM_FEATURE_FLAGS).filter(
({ key }) => row.original.flags[key],
);
const flags = Object.values(SUBSCRIPTION_CLAIM_FEATURE_FLAGS).filter(({ key }) => row.original.flags[key]);
if (flags.length === 0) {
return <p className="text-xs text-muted-foreground">{t`None`}</p>;
return <p className="text-muted-foreground text-xs">{t`None`}</p>;
}
return (
<ul className="list-disc space-y-1 text-xs text-muted-foreground">
<ul className="list-disc space-y-1 text-muted-foreground text-xs">
{flags.map(({ key, label }) => (
<li key={key}>{label}</li>
))}
@@ -1,12 +1,3 @@
import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Role, Subscription } from '@prisma/client';
import { Edit, Loader } from 'lucide-react';
import { Link } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { Button } from '@documenso/ui/primitives/button';
@@ -14,6 +5,13 @@ import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Role, Subscription } from '@prisma/client';
import { Edit, Loader } from 'lucide-react';
import { useEffect, useMemo, useState, useTransition } from 'react';
import { Link } from 'react-router';
type UserData = {
id: number;
@@ -24,10 +22,7 @@ type UserData = {
documentCount: number;
};
type SubscriptionLite = Pick<
Subscription,
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
>;
type SubscriptionLite = Pick<Subscription, 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'>;
type AdminDashboardUsersTableProps = {
users: UserData[];
@@ -36,12 +31,7 @@ type AdminDashboardUsersTableProps = {
page: number;
};
export const AdminDashboardUsersTable = ({
users,
totalPages,
perPage,
page,
}: AdminDashboardUsersTableProps) => {
export const AdminDashboardUsersTable = ({ users, totalPages, perPage, page }: AdminDashboardUsersTableProps) => {
const { _ } = useLingui();
const [isPending, startTransition] = useTransition();
@@ -82,7 +72,7 @@ export const AdminDashboardUsersTable = ({
return (
<Button className="w-24" asChild>
<Link to={`/admin/users/${row.original.id}`}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
<Edit className="mr-2 -ml-1 h-4 w-4" />
<Trans>Edit</Trans>
</Link>
</Button>
@@ -1,9 +1,3 @@
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
@@ -13,6 +7,9 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { Trans, useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
export const AdminDocumentJobsTable = ({ envelopeId }: { envelopeId: string }) => {
const { t, i18n } = useLingui();
@@ -22,12 +19,11 @@ export const AdminDocumentJobsTable = ({ envelopeId }: { envelopeId: string }) =
const parsedSearchParams = ZUrlSearchParamsSchema.parse(Object.fromEntries(searchParams ?? []));
const { data, isLoading, isLoadingError, refetch, isFetching } =
trpc.admin.document.findJobs.useQuery({
envelopeId: envelopeId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
});
const { data, isLoading, isLoadingError, refetch, isFetching } = trpc.admin.document.findJobs.useQuery({
envelopeId: envelopeId,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
});
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
@@ -65,8 +61,7 @@ export const AdminDocumentJobsTable = ({ envelopeId }: { envelopeId: string }) =
{
header: t`Last Retried`,
accessorKey: 'lastRetriedAt',
cell: ({ row }) =>
row.original.lastRetriedAt ? i18n.date(row.original.lastRetriedAt) : 'N/A',
cell: ({ row }) => (row.original.lastRetriedAt ? i18n.date(row.original.lastRetriedAt) : 'N/A'),
},
{
header: t`Completed`,
@@ -79,7 +74,7 @@ export const AdminDocumentJobsTable = ({ envelopeId }: { envelopeId: string }) =
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">
<h2 className="font-semibold text-lg">
<Trans>Background Jobs</Trans>
</h2>
@@ -126,9 +121,7 @@ export const AdminDocumentJobsTable = ({ envelopeId }: { envelopeId: string }) =
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />
}
</DataTable>
</div>
@@ -1,13 +1,3 @@
import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
import { useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -22,6 +12,14 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@documenso/ui/
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { useMemo, useState } from 'react';
import { useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
export type AdminDocumentLogsTableProps = {
envelopeId: string;
@@ -96,15 +94,15 @@ export const AdminDocumentLogsTable = ({ envelopeId }: AdminDocumentLogsTablePro
)}
</div>
) : (
<p>N/A</p>
<p>
<Trans>N/A</Trans>
</p>
),
},
{
header: _(msg`Action`),
accessorKey: 'type',
cell: ({ row }) => (
<span>{formatDocumentAuditLogAction(i18n, row.original).description}</span>
),
cell: ({ row }) => <span>{formatDocumentAuditLogAction(i18n, row.original).description}</span>,
},
{
header: _(msg`IP Address`),
@@ -191,14 +189,14 @@ export const AdminDocumentLogsTable = ({ envelopeId }: AdminDocumentLogsTablePro
{selectedAuditLog && (
<div className="group relative">
<div className="absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100">
<div className="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100">
<CopyTextButton
value={JSON.stringify(selectedAuditLog, null, 2)}
onCopySuccess={() => toast({ title: _(msg`Copied to clipboard`) })}
/>
</div>
<pre className="max-h-[60vh] overflow-auto whitespace-pre-wrap break-all rounded-lg border border-border bg-muted/50 p-4 font-mono text-xs leading-relaxed text-foreground">
<pre className="max-h-[60vh] overflow-auto whitespace-pre-wrap break-all rounded-lg border border-border bg-muted/50 p-4 font-mono text-foreground text-xs leading-relaxed">
{JSON.stringify(selectedAuditLog, null, 2)}
</pre>
</div>
@@ -1,31 +1,33 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Field, type Recipient, type Signature, SigningStatus } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod';
import { zEmail } from '@documenso/lib/utils/zod';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Field, type Recipient, RecipientRole, type Signature, SigningStatus } from '@prisma/client';
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { z } from 'zod';
const RECIPIENT_ROLE_LABELS: Record<RecipientRole, string> = {
[RecipientRole.SIGNER]: 'Signer',
[RecipientRole.APPROVER]: 'Approver',
[RecipientRole.CC]: 'CC',
[RecipientRole.VIEWER]: 'Viewer',
[RecipientRole.ASSISTANT]: 'Assistant',
};
const ZAdminUpdateRecipientFormSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
email: zEmail(),
role: z.nativeEnum(RecipientRole),
});
type TAdminUpdateRecipientFormSchema = z.infer<typeof ZAdminUpdateRecipientFormSchema>;
@@ -49,6 +51,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
defaultValues: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
},
});
@@ -81,9 +84,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
accessorKey: 'signature',
cell: ({ row }) => (
<div>
{row.original.signature?.typedSignature && (
<span>{row.original.signature.typedSignature}</span>
)}
{row.original.signature?.typedSignature && <span>{row.original.signature.typedSignature}</span>}
{row.original.signature?.signatureImageAsBase64 && (
<img
@@ -98,12 +99,13 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
] satisfies DataTableColumnDef<(typeof recipient)['fields'][number]>[];
}, []);
const onUpdateRecipientFormSubmit = async ({ name, email }: TAdminUpdateRecipientFormSchema) => {
const onUpdateRecipientFormSubmit = async ({ name, email, role }: TAdminUpdateRecipientFormSchema) => {
try {
await updateRecipient({
id: recipient.id,
name,
email,
role,
});
toast({
@@ -127,9 +129,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
<form onSubmit={form.handleSubmit(onUpdateRecipientFormSubmit)}>
<fieldset
className="flex h-full max-w-xl flex-col gap-y-4"
disabled={
form.formState.isSubmitting || recipient.signingStatus === SigningStatus.SIGNED
}
disabled={form.formState.isSubmitting || recipient.signingStatus === SigningStatus.SIGNED}
>
<FormField
control={form.control}
@@ -167,6 +167,40 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>
<Trans>Role</Trans>
</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={field.onChange}
disabled={form.formState.isSubmitting || recipient.signingStatus === SigningStatus.SIGNED}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.values(RecipientRole).map((role) => (
<SelectItem key={role} value={role}>
{RECIPIENT_ROLE_LABELS[role]}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update Recipient</Trans>
@@ -178,7 +212,7 @@ export const AdminDocumentRecipientItemTable = ({ recipient }: RecipientItemProp
<hr className="my-4" />
<h2 className="mb-4 text-lg font-semibold">
<h2 className="mb-4 font-semibold text-lg">
<Trans>Fields</Trans>
</h2>
@@ -1,11 +1,3 @@
import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDownIcon, ChevronUpIcon, ChevronsUpDown, Loader } from 'lucide-react';
import { Link } from 'react-router';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { DateRange } from '@documenso/lib/types/search-params';
@@ -13,6 +5,12 @@ import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Input } from '@documenso/ui/primitives/input';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { ChevronDownIcon, ChevronsUpDown, ChevronUpIcon, Loader } from 'lucide-react';
import { useEffect, useMemo, useState, useTransition } from 'react';
import { Link } from 'react-router';
export type OrganisationOverview = {
id: string;
@@ -56,10 +54,7 @@ export const AdminOrganisationOverviewTable = ({
return [
{
header: () => (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('name')}
>
<div className="flex cursor-pointer items-center" onClick={() => handleColumnSort('name')}>
{_(msg`Name`)}
{sortBy === 'name' ? (
sortOrder === 'asc' ? (
@@ -89,10 +84,7 @@ export const AdminOrganisationOverviewTable = ({
},
{
header: () => (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('signingVolume')}
>
<div className="flex cursor-pointer items-center" onClick={() => handleColumnSort('signingVolume')}>
<span className="whitespace-nowrap">
<Trans>Document Volume</Trans>
</span>
@@ -130,10 +122,7 @@ export const AdminOrganisationOverviewTable = ({
{
header: () => {
return (
<div
className="flex cursor-pointer items-center"
onClick={() => handleColumnSort('createdAt')}
>
<div className="flex cursor-pointer items-center" onClick={() => handleColumnSort('createdAt')}>
<span className="whitespace-nowrap">
<Trans>Created</Trans>
</span>
@@ -1,16 +1,3 @@
import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import {
CreditCardIcon,
ExternalLinkIcon,
MoreHorizontalIcon,
SettingsIcon,
UserIcon,
} from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { SUBSCRIPTION_STATUS_MAP } from '@documenso/lib/constants/billing';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -28,6 +15,19 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { Trans, useLingui } from '@lingui/react/macro';
import {
ArrowRightLeftIcon,
CreditCardIcon,
ExternalLinkIcon,
MoreHorizontalIcon,
SettingsIcon,
UserIcon,
} from 'lucide-react';
import { useMemo, useState } from 'react';
import { Link, useSearchParams } from 'react-router';
import { AdminSwapSubscriptionDialog } from '~/components/dialogs/admin-swap-subscription-dialog';
type AdminOrganisationsTableOptions = {
ownerUserId?: number;
@@ -44,6 +44,12 @@ export const AdminOrganisationsTable = ({
}: AdminOrganisationsTableOptions) => {
const { t, i18n } = useLingui();
const [swapSource, setSwapSource] = useState<{
id: string;
name: string;
ownerId: number;
} | null>(null);
const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
@@ -76,9 +82,7 @@ export const AdminOrganisationsTable = ({
{
header: t`Organisation`,
accessorKey: 'name',
cell: ({ row }) => (
<Link to={`/admin/organisations/${row.original.id}`}>{row.original.name}</Link>
),
cell: ({ row }) => <Link to={`/admin/organisations/${row.original.id}`}>{row.original.name}</Link>,
},
{
header: t`Created At`,
@@ -88,17 +92,13 @@ export const AdminOrganisationsTable = ({
{
header: t`Owner`,
accessorKey: 'owner',
cell: ({ row }) => (
<Link to={`/admin/users/${row.original.owner.id}`}>{row.original.owner.name}</Link>
),
cell: ({ row }) => <Link to={`/admin/users/${row.original.owner.id}`}>{row.original.owner.name}</Link>,
},
{
id: 'role',
header: t`Role`,
cell: ({ row }) => (
<Badge variant="neutral">
{row.original.owner.id === memberUserId ? t`Owner` : t`Member`}
</Badge>
<Badge variant="neutral">{row.original.owner.id === memberUserId ? t`Owner` : t`Member`}</Badge>
),
},
{
@@ -109,7 +109,7 @@ export const AdminOrganisationsTable = ({
const isPaid = subscription && subscription.status === 'ACTIVE';
return (
<div
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
className={`inline-flex items-center rounded-full px-2 py-1 font-medium text-xs ${
isPaid ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`}
>
@@ -131,7 +131,7 @@ export const AdminOrganisationsTable = ({
target="_blank"
className="flex flex-row items-center gap-2"
>
{SUBSCRIPTION_STATUS_MAP[row.original.subscription.status]}
{i18n._(SUBSCRIPTION_STATUS_MAP[row.original.subscription.status])}
<ExternalLinkIcon className="h-4 w-4" />
</Link>
) : (
@@ -143,7 +143,7 @@ export const AdminOrganisationsTable = ({
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
<MoreHorizontalIcon className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -172,12 +172,28 @@ export const AdminOrganisationsTable = ({
{!row.original.customerId && <span>&nbsp;(N/A)</span>}
</Link>
</DropdownMenuItem>
{row.original.subscription &&
(row.original.subscription.status === 'ACTIVE' || row.original.subscription.status === 'PAST_DUE') && (
<DropdownMenuItem
onClick={() =>
setSwapSource({
id: row.original.id,
name: row.original.name,
ownerId: row.original.owner.id,
})
}
>
<ArrowRightLeftIcon className="mr-2 h-4 w-4" />
<Trans>Move Subscription</Trans>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
),
},
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
}, []);
}, [i18n, t, memberUserId, showOwnerColumn]);
return (
<div>
@@ -227,6 +243,20 @@ export const AdminOrganisationsTable = ({
) : null
}
</DataTable>
{swapSource && (
<AdminSwapSubscriptionDialog
sourceOrganisationId={swapSource.id}
sourceOrganisationName={swapSource.name}
userId={swapSource.ownerId}
open={true}
onOpenChange={(open) => {
if (!open) {
setSwapSource(null);
}
}}
/>
)}
</div>
);
};
@@ -1,9 +1,3 @@
import { useMemo } from 'react';
import { useLingui } from '@lingui/react';
import { useLingui as useLinguiMacro } from '@lingui/react/macro';
import { Link, useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -16,6 +10,10 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@documenso/ui/primitives/hover-card';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useLingui } from '@lingui/react';
import { useLingui as useLinguiMacro } from '@lingui/react/macro';
import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router';
type AdminUserTeamsTableProps = {
userId: number;
@@ -61,10 +59,7 @@ export const AdminUserTeamsTable = ({ userId }: AdminUserTeamsTableProps) => {
<HoverCardTrigger className="cursor-default underline decoration-dotted underline-offset-4">
{row.original.name}
</HoverCardTrigger>
<HoverCardContent
className="w-auto font-mono text-xs text-muted-foreground"
align="start"
>
<HoverCardContent className="w-auto font-mono text-muted-foreground text-xs" align="start">
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
<dt>id</dt>
<dd>{row.original.id}</dd>
@@ -79,10 +74,7 @@ export const AdminUserTeamsTable = ({ userId }: AdminUserTeamsTableProps) => {
header: t`Organisation`,
accessorKey: 'organisation',
cell: ({ row }) => (
<Link
to={`/admin/organisations/${row.original.organisation.id}`}
className="hover:underline"
>
<Link to={`/admin/organisations/${row.original.organisation.id}`} className="hover:underline">
{row.original.organisation.name}
</Link>
),
@@ -91,9 +83,7 @@ export const AdminUserTeamsTable = ({ userId }: AdminUserTeamsTableProps) => {
header: t`Role`,
accessorKey: 'teamRole',
cell: ({ row }) => (
<Badge variant="neutral">
{i18n._(TEAM_MEMBER_ROLE_MAP[row.original.teamRole as TeamMemberRole])}
</Badge>
<Badge variant="neutral">{i18n._(TEAM_MEMBER_ROLE_MAP[row.original.teamRole as TeamMemberRole])}</Badge>
),
},
{
@@ -137,9 +127,7 @@ export const AdminUserTeamsTable = ({ userId }: AdminUserTeamsTableProps) => {
}}
>
{(table) =>
table.getPageCount() > 1 ? (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
) : null
table.getPageCount() > 1 ? <DataTablePagination additionalInformation="VisibleCount" table={table} /> : null
}
</DataTable>
);
@@ -1,12 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import type { DateTimeFormatOptions } from 'luxon';
import { useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
@@ -16,6 +7,14 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
export type DocumentLogsTableProps = {
documentId: number;
@@ -88,15 +87,15 @@ export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps
)}
</div>
) : (
<p>N/A</p>
<p>
<Trans>N/A</Trans>
</p>
),
},
{
header: _(msg`Action`),
accessorKey: 'type',
cell: ({ row }) => (
<span>{formatDocumentAuditLogAction(i18n, row.original, userId).description}</span>
),
cell: ({ row }) => <span>{formatDocumentAuditLogAction(i18n, row.original, userId).description}</span>,
},
{
header: _(msg`IP Address`),
@@ -1,15 +1,15 @@
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { findRecipientByEmail } from '@documenso/lib/utils/recipients';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Button } from '@documenso/ui/primitives/button';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Button } from '@documenso/ui/primitives/button';
import { useCurrentTeam } from '~/providers/team';
import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog';
@@ -23,7 +23,11 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
const team = useCurrentTeam();
const recipient = row.recipients.find((recipient) => recipient.email === user.email);
const recipient = findRecipientByEmail({
recipients: row.recipients,
userEmail: user.email,
teamEmail: team.teamEmail?.email,
});
const isOwner = row.user.id === user.id;
const isRecipient = !!recipient;
@@ -52,36 +56,33 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
isCurrentTeamDocument,
internalVersion: row.internalVersion,
})
.with(
isOwner ? { isDraft: true, isOwner: true } : { isDraft: true, isCurrentTeamDocument: true },
() => (
<Button className="w-32" asChild>
<Link to={formatPath}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</Link>
</Button>
),
)
.with(isOwner ? { isDraft: true, isOwner: true } : { isDraft: true, isCurrentTeamDocument: true }, () => (
<Button className="w-32" asChild>
<Link to={formatPath}>
<Edit className="mr-2 -ml-1 h-4 w-4" />
<Trans>Edit</Trans>
</Link>
</Button>
))
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
<Button className="w-32" asChild>
<Link to={`/sign/${recipient?.token}`}>
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
<Pencil className="-ml-1 mr-2 h-4 w-4" />
<Pencil className="mr-2 -ml-1 h-4 w-4" />
<Trans>Sign</Trans>
</>
))
.with(RecipientRole.APPROVER, () => (
<>
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
<CheckCircle className="mr-2 -ml-1 h-4 w-4" />
<Trans>Approve</Trans>
</>
))
.otherwise(() => (
<>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
<EyeIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>View</Trans>
</>
))}
@@ -90,7 +91,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
))
.with({ isPending: true, isSigned: true }, () => (
<Button className="w-32" disabled={true}>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
<EyeIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>View</Trans>
</Button>
))
@@ -101,7 +102,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
token={recipient?.token}
trigger={
<Button className="w-32">
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Download className="mr-2 -ml-1 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
}
@@ -1,28 +1,10 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, RecipientRole } from '@prisma/client';
import {
CheckCircle,
Copy,
Download,
Edit,
EyeIcon,
FolderInput,
Loader,
MoreHorizontal,
Pencil,
Share,
Trash2,
} from 'lucide-react';
import { Link } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
import { findRecipientByEmail } from '@documenso/lib/utils/recipients';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcReact } from '@documenso/trpc/react';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import {
DropdownMenu,
@@ -31,33 +13,57 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, EnvelopeType, RecipientRole } from '@prisma/client';
import {
CheckCircle,
Copy,
Download,
Edit,
EyeIcon,
FileOutputIcon,
FolderInput,
Loader,
MoreHorizontal,
Pencil,
Share,
Trash2,
} from 'lucide-react';
import { useState } from 'react';
import { Link } from 'react-router';
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
import { EnvelopeDeleteDialog } from '~/components/dialogs/envelope-delete-dialog';
import { EnvelopeDuplicateDialog } from '~/components/dialogs/envelope-duplicate-dialog';
import { EnvelopeSaveAsTemplateDialog } from '~/components/dialogs/envelope-save-as-template-dialog';
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
import { useCurrentTeam } from '~/providers/team';
import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog';
import { EnvelopeRenameDialog } from '../dialogs/envelope-rename-dialog';
export type DocumentsTableActionDropdownProps = {
row: TDocumentRow;
onMoveDocument?: () => void;
};
export const DocumentsTableActionDropdown = ({
row,
onMoveDocument,
}: DocumentsTableActionDropdownProps) => {
export const DocumentsTableActionDropdown = ({ row, onMoveDocument }: DocumentsTableActionDropdownProps) => {
const { user } = useSession();
const team = useCurrentTeam();
const { _ } = useLingui();
const trpcUtils = trpcReact.useUtils();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
const [isRenameDialogOpen, setRenameDialogOpen] = useState(false);
const [isSaveAsTemplateDialogOpen, setSaveAsTemplateDialogOpen] = useState(false);
const recipient = row.recipients.find((recipient) => recipient.email === user.email);
const recipient = findRecipientByEmail({
recipients: row.recipients,
userEmail: user.email,
teamEmail: team.teamEmail?.email,
});
const isOwner = row.user.id === user.id;
// const isRecipient = !!recipient;
@@ -68,6 +74,16 @@ export const DocumentsTableActionDropdown = ({
const isCurrentTeamDocument = team && row.team?.url === team.url;
const canManageDocument = Boolean(isOwner || isCurrentTeamDocument);
const { canTitleBeChanged } = getEnvelopeItemPermissions(
{
completedAt: row.completedAt,
deletedAt: row.deletedAt,
type: EnvelopeType.DOCUMENT,
status: row.status,
},
[],
);
const documentsPath = formatDocumentsPath(team.url);
const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
@@ -76,7 +92,7 @@ export const DocumentsTableActionDropdown = ({
return (
<DropdownMenu>
<DropdownMenuTrigger data-testid="document-table-action-btn">
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -121,10 +137,18 @@ export const DocumentsTableActionDropdown = ({
</Link>
</DropdownMenuItem>
{canManageDocument && canTitleBeChanged && (
<DropdownMenuItem onClick={() => setRenameDialogOpen(true)}>
<Pencil className="mr-2 h-4 w-4" />
<Trans>Rename</Trans>
</DropdownMenuItem>
)}
<EnvelopeDownloadDialog
envelopeId={row.envelopeId}
envelopeStatus={row.status}
token={recipient?.token}
isLegacy={row.internalVersion === 1}
token={canManageDocument ? undefined : recipient?.token}
trigger={
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
@@ -135,9 +159,22 @@ export const DocumentsTableActionDropdown = ({
}
/>
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
<Copy className="mr-2 h-4 w-4" />
<Trans>Duplicate</Trans>
<EnvelopeDuplicateDialog
envelopeId={row.envelopeId}
envelopeType={EnvelopeType.DOCUMENT}
trigger={
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<Copy className="mr-2 h-4 w-4" />
<Trans>Duplicate</Trans>
</div>
</DropdownMenuItem>
}
/>
<DropdownMenuItem onClick={() => setSaveAsTemplateDialogOpen(true)}>
<FileOutputIcon className="mr-2 h-4 w-4" />
<Trans>Save as Template</Trans>
</DropdownMenuItem>
{onMoveDocument && canManageDocument && (
@@ -153,10 +190,21 @@ export const DocumentsTableActionDropdown = ({
Void
</DropdownMenuItem> */}
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}>
<Trash2 className="mr-2 h-4 w-4" />
{canManageDocument ? _(msg`Delete`) : _(msg`Hide`)}
</DropdownMenuItem>
<EnvelopeDeleteDialog
id={row.envelopeId}
type={EnvelopeType.DOCUMENT}
status={row.status}
title={row.title}
canManageDocument={canManageDocument}
trigger={
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<Trash2 className="mr-2 h-4 w-4" />
{canManageDocument ? _(msg`Delete`) : _(msg`Hide`)}
</div>
</DropdownMenuItem>
}
/>
<DropdownMenuLabel>
<Trans>Share</Trans>
@@ -192,20 +240,20 @@ export const DocumentsTableActionDropdown = ({
/>
</DropdownMenuContent>
<DocumentDeleteDialog
id={row.id}
status={row.status}
documentTitle={row.title}
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
canManageDocument={canManageDocument}
<EnvelopeSaveAsTemplateDialog
envelopeId={row.envelopeId}
open={isSaveAsTemplateDialogOpen}
onOpenChange={setSaveAsTemplateDialogOpen}
/>
<DocumentDuplicateDialog
<EnvelopeRenameDialog
id={row.envelopeId}
token={recipient?.token}
open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen}
initialTitle={row.title}
open={isRenameDialogOpen}
onOpenChange={setRenameDialogOpen}
onSuccess={async () => {
await trpcUtils.document.findDocumentsInternal.invalidate();
}}
/>
</DropdownMenu>
);
@@ -1,10 +1,9 @@
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Bird, CheckCircle2 } from 'lucide-react';
import { match } from 'ts-pattern';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export type DocumentsTableEmptyStateProps = { status: ExtendedDocumentStatus };
export const DocumentsTableEmptyState = ({ status }: DocumentsTableEmptyStateProps) => {
@@ -38,13 +37,13 @@ export const DocumentsTableEmptyState = ({ status }: DocumentsTableEmptyStatePro
return (
<div
className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4"
className="flex h-60 flex-col items-center justify-center gap-y-4 text-muted-foreground/60"
data-testid="empty-document-state"
>
<Icon className="h-12 w-12" strokeWidth={1.5} />
<div className="text-center">
<h3 className="text-lg font-semibold">{_(title)}</h3>
<h3 className="font-semibold text-lg">{_(title)}</h3>
<p className="mt-2 max-w-[60ch]">{_(message)}</p>
</div>
@@ -1,10 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { trpc } from '@documenso/trpc/react';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { useLocation, useNavigate, useSearchParams } from 'react-router';
type DocumentsTableSenderFilterProps = {
teamId: number;
@@ -17,9 +16,7 @@ export const DocumentsTableSenderFilter = ({ teamId }: DocumentsTableSenderFilte
const isMounted = useIsMounted();
const senderIds = (searchParams?.get('senderIds') ?? '')
.split(',')
.filter((value) => value !== '');
const senderIds = (searchParams?.get('senderIds') ?? '').split(',').filter((value) => value !== '');
const { data, isLoading } = trpc.team.member.getMany.useQuery({
teamId,
@@ -49,7 +46,7 @@ export const DocumentsTableSenderFilter = ({ teamId }: DocumentsTableSenderFilte
return (
<MultiSelectCombobox
emptySelectionPlaceholder={
<p className="text-muted-foreground font-normal">
<p className="font-normal text-muted-foreground">
<Trans>
<span className="text-muted-foreground/70">Sender:</span> All
</Trans>
@@ -1,9 +1,11 @@
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { findRecipientByEmail } from '@documenso/lib/utils/recipients';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { useCurrentTeam } from '~/providers/team';
export type DataTableTitleProps = {
row: TDocumentRow;
@@ -12,8 +14,13 @@ export type DataTableTitleProps = {
export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
const { user } = useSession();
const team = useCurrentTeam();
const recipient = row.recipients.find((recipient) => recipient.email === user.email);
const recipient = findRecipientByEmail({
recipients: row.recipients,
userEmail: user.email,
teamEmail: team.teamEmail?.email,
});
const isOwner = row.user.id === user.id;
const isRecipient = !!recipient;
@@ -45,8 +52,6 @@ export const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
</Link>
))
.otherwise(() => (
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{row.title}
</span>
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">{row.title}</span>
));
};
@@ -1,15 +1,7 @@
import { useMemo, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { findRecipientByEmail } from '@documenso/lib/utils/recipients';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { TFindDocumentsResponse } from '@documenso/trpc/server/document-router/find-documents.types';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
@@ -18,6 +10,13 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { DateTime } from 'luxon';
import { useMemo, useTransition } from 'react';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { DocumentStatus } from '~/components/general/document/document-status';
import { useCurrentTeam } from '~/providers/team';
@@ -86,12 +85,11 @@ export const DocumentsTable = ({
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) =>
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
cell: ({ row }) => i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
},
{
header: _(msg`Title`),
cell: ({ row }) => <DataTableTitle row={row.original} teamUrl={team?.url} />,
cell: ({ row }) => <DataTableTitle row={row.original} teamUrl={team?.url} teamEmail={team?.teamEmail?.email} />,
},
{
id: 'sender',
@@ -102,10 +100,7 @@ export const DocumentsTable = ({
header: _(msg`Recipient`),
accessorKey: 'recipient',
cell: ({ row }) => (
<StackAvatarsWithTooltip
recipients={row.original.recipients}
documentStatus={row.original.status}
/>
<StackAvatarsWithTooltip recipients={row.original.recipients} documentStatus={row.original.status} />
),
},
{
@@ -213,12 +208,17 @@ export const DocumentsTable = ({
type DataTableTitleProps = {
row: DocumentsTableRow;
teamUrl: string;
teamEmail?: string;
};
const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
const DataTableTitle = ({ row, teamUrl, teamEmail }: DataTableTitleProps) => {
const { user } = useSession();
const recipient = row.recipients.find((recipient) => recipient.email === user.email);
const recipient = findRecipientByEmail({
recipients: row.recipients,
userEmail: user.email,
teamEmail,
});
const isOwner = row.user.id === user.id;
const isRecipient = !!recipient;
@@ -251,8 +251,6 @@ const DataTableTitle = ({ row, teamUrl }: DataTableTitleProps) => {
</Link>
))
.otherwise(() => (
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{row.title}
</span>
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">{row.title}</span>
));
};
@@ -1,8 +1,6 @@
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { FolderInputIcon, Trash2Icon, XIcon } from 'lucide-react';
import { Button } from '@documenso/ui/primitives/button';
import { Trans, useLingui } from '@lingui/react/macro';
import { FolderInputIcon, Trash2Icon, XIcon } from 'lucide-react';
export type EnvelopesTableBulkActionBarProps = {
selectedCount: number;
@@ -25,7 +23,7 @@ export const EnvelopesTableBulkActionBar = ({
return (
<div className="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-x-4 rounded-lg border border-border bg-background px-4 py-3 shadow-lg">
<span className="text-sm font-medium">
<span className="font-medium text-sm">
<Trans>{selectedCount} selected</Trans>
</span>
@@ -1,15 +1,3 @@
import { useMemo, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus as DocumentStatusEnum } from '@prisma/client';
import { RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircleIcon, DownloadIcon, EyeIcon, Loader, PencilIcon } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
@@ -22,6 +10,15 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus as DocumentStatusEnum, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircleIcon, DownloadIcon, EyeIcon, Loader, PencilIcon } from 'lucide-react';
import { DateTime } from 'luxon';
import { useMemo, useTransition } from 'react';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import { DocumentStatus } from '~/components/general/document/document-status';
import { useOptionalCurrentTeam } from '~/providers/team';
@@ -59,15 +56,12 @@ export const InboxTable = () => {
{
header: _(msg`Created`),
accessorKey: 'createdAt',
cell: ({ row }) =>
i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
cell: ({ row }) => i18n.date(row.original.createdAt, { ...DateTime.DATETIME_SHORT, hourCycle: 'h12' }),
},
{
header: _(msg`Title`),
cell: ({ row }) => (
<span className="block max-w-[10rem] truncate font-medium md:max-w-[20rem]">
{row.original.title}
</span>
<span className="block max-w-[10rem] truncate font-medium md:max-w-[20rem]">{row.original.title}</span>
),
},
{
@@ -79,10 +73,7 @@ export const InboxTable = () => {
header: _(msg`Recipient`),
accessorKey: 'recipient',
cell: ({ row }) => (
<StackAvatarsWithTooltip
recipients={row.original.recipients}
documentStatus={row.original.status}
/>
<StackAvatarsWithTooltip recipients={row.original.recipients} documentStatus={row.original.status} />
),
},
{
@@ -130,7 +121,7 @@ export const InboxTable = () => {
enable: isLoadingError || false,
}}
emptyState={
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
<div className="flex h-60 flex-col items-center justify-center gap-y-4 text-muted-foreground/60">
<p>
<Trans>Documents that require your attention will appear here</Trans>
</p>
@@ -163,15 +154,13 @@ export const InboxTable = () => {
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />
}
</DataTable>
{isPending && (
<div className="bg-background/50 absolute inset-0 flex items-center justify-center">
<Loader className="text-muted-foreground h-8 w-8 animate-spin" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -215,19 +204,19 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
{match(role)
.with(RecipientRole.SIGNER, () => (
<>
<PencilIcon className="-ml-1 mr-2 h-4 w-4" />
<PencilIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>Sign</Trans>
</>
))
.with(RecipientRole.APPROVER, () => (
<>
<CheckCircleIcon className="-ml-1 mr-2 h-4 w-4" />
<CheckCircleIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>Approve</Trans>
</>
))
.otherwise(() => (
<>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
<EyeIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>View</Trans>
</>
))}
@@ -236,7 +225,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
))
.with({ isPending: true, isSigned: true }, () => (
<Button className="w-32" disabled={true}>
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
<EyeIcon className="mr-2 -ml-1 h-4 w-4" />
<Trans>View</Trans>
</Button>
))
@@ -247,7 +236,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
token={recipient?.token}
trigger={
<Button className="w-32">
<DownloadIcon className="-ml-1 mr-2 inline h-4 w-4" />
<DownloadIcon className="mr-2 -ml-1 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
}
@@ -1,19 +1,15 @@
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { DOCUMENT_AUDIT_LOG_TYPE, type TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { P, match } from 'ts-pattern';
import { match, P } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import {
DOCUMENT_AUDIT_LOG_TYPE,
type TDocumentAuditLog,
} from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
export type AuditLogDataTableProps = {
logs: TDocumentAuditLog[];
};
@@ -33,10 +29,7 @@ const getAuditLogIndicatorColor = (type: string) =>
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => 'bg-red-500')
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, () => 'bg-orange-500')
.with(
P.union(
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
),
P.union(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED),
() => 'bg-blue-500',
)
.otherwise(() => 'bg-muted');
@@ -90,22 +83,20 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
{/* Header Section with indicator, event type, and timestamp */}
<div className="mb-3 flex items-start justify-between">
<div className="flex items-baseline gap-3">
<div
className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))}
/>
<div className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))} />
<div>
<div className="text-sm font-medium uppercase tracking-wide text-muted-foreground print:text-[8pt]">
<div className="font-medium text-muted-foreground text-sm uppercase tracking-wide print:text-[8pt]">
{log.type.replace(/_/g, ' ')}
</div>
<div className="text-sm font-medium text-foreground print:text-[8pt]">
<div className="font-medium text-foreground text-sm print:text-[8pt]">
{formattedAction.description}
</div>
</div>
</div>
<div className="text-sm text-muted-foreground print:text-[8pt]">
<div className="text-muted-foreground text-sm print:text-[8pt]">
{DateTime.fromJSDate(log.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
@@ -117,15 +108,13 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
{/* Details Section - Two column layout */}
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
<div>
<div className="font-medium uppercase tracking-wide text-muted-foreground/70">
{_(msg`User`)}
</div>
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">{_(msg`User`)}</div>
<div className="mt-1 font-mono text-foreground">{log.email || 'N/A'}</div>
</div>
<div className="text-right">
<div className="font-medium uppercase tracking-wide text-muted-foreground/70">
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
{_(msg`IP Address`)}
</div>
@@ -133,13 +122,11 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
</div>
<div className="col-span-2">
<div className="font-medium uppercase tracking-wide text-muted-foreground/70">
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
{_(msg`User Agent`)}
</div>
<div className="mt-1 text-foreground">
{_(formatUserAgent(log.userAgent, userAgentInfo))}
</div>
<div className="mt-1 text-foreground">{_(formatUserAgent(log.userAgent, userAgentInfo))}</div>
</div>
</div>
</CardContent>
@@ -1,18 +1,16 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { File } from 'lucide-react';
import { DateTime } from 'luxon';
import { Link } from 'react-router';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { File } from 'lucide-react';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { Link } from 'react-router';
export type OrganisationBillingInvoicesTableProps = {
organisationId: string;
@@ -59,7 +57,7 @@ export const OrganisationBillingInvoicesTable = ({
<div className="flex max-w-xs items-center gap-2">
<File className="h-6 w-6" />
<div className="text-foreground/80 text-sm font-semibold">
<div className="font-semibold text-foreground/80 text-sm">
{DateTime.fromSeconds(row.original.created).toFormat('MMMM yyyy')}
</div>
</div>
@@ -87,21 +85,13 @@ export const OrganisationBillingInvoicesTable = ({
id: 'actions',
cell: ({ row }) => (
<div className="flex justify-end space-x-2">
<Button
variant="outline"
asChild
disabled={typeof row.original.hosted_invoice_url !== 'string'}
>
<Button variant="outline" asChild disabled={typeof row.original.hosted_invoice_url !== 'string'}>
<Link to={row.original.hosted_invoice_url ?? ''} target="_blank">
<Trans>View</Trans>
</Link>
</Button>
<Button
variant="outline"
asChild
disabled={typeof row.original.invoice_pdf !== 'string'}
>
<Button variant="outline" asChild disabled={typeof row.original.invoice_pdf !== 'string'}>
<Link to={row.original.invoice_pdf ?? ''} target="_blank">
<Trans>Download</Trans>
</Link>
@@ -1,11 +1,3 @@
import { useMemo } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { EmailDomainStatus } from '@prisma/client';
import { CheckCircle2Icon, ClockIcon } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -20,6 +12,12 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import { EmailDomainStatus } from '@prisma/client';
import { CheckCircle2Icon, ClockIcon } from 'lucide-react';
import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern';
import { OrganisationEmailDomainDeleteDialog } from '../dialogs/organisation-email-domain-delete-dialog';
@@ -43,18 +41,17 @@ export const OrganisationEmailDomainsDataTable = () => {
},
});
const { data, isLoading, isLoadingError } =
trpc.enterprise.organisation.emailDomain.find.useQuery(
{
organisationId: organisation.id,
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
placeholderData: (previousData) => previousData,
},
);
const { data, isLoading, isLoadingError } = trpc.enterprise.organisation.emailDomain.find.useQuery(
{
organisationId: organisation.id,
query: parsedSearchParams.query,
page: parsedSearchParams.page,
perPage: parsedSearchParams.perPage,
},
{
placeholderData: (previousData) => previousData,
},
);
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
@@ -105,9 +102,7 @@ export const OrganisationEmailDomainsDataTable = () => {
cell: ({ row }) => (
<div className="flex justify-end space-x-2">
<Button asChild variant="outline">
<Link to={`/o/${organisation.url}/settings/email-domains/${row.original.id}`}>
Manage
</Link>
<Link to={`/o/${organisation.url}/settings/email-domains/${row.original.id}`}>Manage</Link>
</Button>
<OrganisationEmailDomainDeleteDialog
@@ -162,27 +157,20 @@ export const OrganisationEmailDomainsDataTable = () => {
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />
}
</DataTable>
<AnimateGenericFadeInOut key={results.data.length}>
{results.data.length > 0 && (
<Alert
className="mt-2 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
variant="neutral"
>
<Alert className="mt-2 flex flex-col justify-between p-6 sm:flex-row sm:items-center" variant="neutral">
<div className="mb-4 sm:mb-0">
<AlertTitle>
<Trans>Sync Email Domains</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
This will check and sync the status of all email domains for this organisation
</Trans>
<Trans>This will check and sync the status of all email domains for this organisation</Trans>
</AlertDescription>
</div>
@@ -1,11 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType } from '@prisma/client';
import { Link, useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { EXTENDED_ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
@@ -17,6 +9,12 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType } from '@prisma/client';
import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router';
import { OrganisationGroupDeleteDialog } from '../dialogs/organisation-group-delete-dialog';
@@ -141,11 +139,7 @@ export const OrganisationGroupsDataTable = () => {
),
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
}
{(table) => results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
);
};
@@ -1,11 +1,3 @@
import { useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Building2, Loader, TrendingUp, Users } from 'lucide-react';
import { Link } from 'react-router';
import { useNavigation } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import type { OrganisationDetailedInsights } from '@documenso/lib/server-only/admin/get-organisation-detailed-insights';
import type { DateRange } from '@documenso/lib/types/search-params';
@@ -14,6 +6,11 @@ import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Building2, Loader, TrendingUp, Users } from 'lucide-react';
import { useTransition } from 'react';
import { Link, useNavigation } from 'react-router';
import { DateRangeFilter } from '~/components/filters/date-range-filter';
import { DocumentStatus } from '~/components/general/document/document-status';
@@ -62,7 +59,11 @@ export const OrganisationInsightsTable = ({
{
header: _(msg`Team Name`),
accessorKey: 'name',
cell: ({ row }) => <span className="block max-w-full truncate">{row.getValue('name')}</span>,
cell: ({ row }) => (
<Link className="block max-w-full truncate hover:underline" to={`/admin/teams/${row.original.id}`}>
{row.getValue('name')}
</Link>
),
size: 240,
},
{
@@ -90,10 +91,7 @@ export const OrganisationInsightsTable = ({
header: () => <span className="whitespace-nowrap">{_(msg`Name`)}</span>,
accessorKey: 'name',
cell: ({ row }) => (
<Link
className="block max-w-full truncate hover:underline"
to={`/admin/users/${row.original.id}`}
>
<Link className="block max-w-full truncate hover:underline" to={`/admin/users/${row.original.id}`}>
{(row.getValue('name') as string) || (row.getValue('email') as string)}
</Link>
),
@@ -143,9 +141,7 @@ export const OrganisationInsightsTable = ({
{
header: () => <span className="whitespace-nowrap">{_(msg`Status`)}</span>,
accessorKey: 'status',
cell: ({ row }) => (
<DocumentStatus status={row.getValue('status') as ExtendedDocumentStatus} />
),
cell: ({ row }) => <DocumentStatus status={row.getValue('status') as ExtendedDocumentStatus} />,
size: 120,
},
{
@@ -276,11 +272,11 @@ const SummaryCard = ({
value: number;
subtitle?: string;
}) => (
<div className="bg-card flex items-start gap-x-2 rounded-lg border px-4 py-3">
<Icon className="text-muted-foreground h-4 w-4 items-start" />
<div className="flex items-start gap-x-2 rounded-lg border bg-card px-4 py-3">
<Icon className="h-4 w-4 items-start text-muted-foreground" />
<div className="-mt-0.5 space-y-2">
<p className="text-muted-foreground text-sm font-medium">{title}</p>
<p className="text-2xl font-bold">{value}</p>
<p className="font-medium text-muted-foreground text-sm">{title}</p>
<p className="font-bold text-2xl">{value}</p>
{subtitle && <p className="text-muted-foreground text-xs">{subtitle}</p>}
</div>
</div>
@@ -1,16 +1,8 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { History, MoreHorizontal, Trash2 } from 'lucide-react';
import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
@@ -26,6 +18,13 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationMemberInviteStatus } from '@prisma/client';
import { History, MoreHorizontal, Trash2 } from 'lucide-react';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
export const OrganisationMemberInvitesTable = () => {
const [searchParams] = useSearchParams();
@@ -50,39 +49,37 @@ export const OrganisationMemberInvitesTable = () => {
},
);
const { mutateAsync: resendOrganisationMemberInvitation } =
trpc.organisation.member.invite.resend.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Invitation has been resent`),
});
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Unable to resend invitation. Please try again.`),
variant: 'destructive',
});
},
});
const { mutateAsync: resendOrganisationMemberInvitation } = trpc.organisation.member.invite.resend.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Invitation has been resent`),
});
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Unable to resend invitation. Please try again.`),
variant: 'destructive',
});
},
});
const { mutateAsync: deleteOrganisationMemberInvitations } =
trpc.organisation.member.invite.deleteMany.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Invitation has been deleted`),
});
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Unable to delete invitation. Please try again.`),
variant: 'destructive',
});
},
});
const { mutateAsync: deleteOrganisationMemberInvitations } = trpc.organisation.member.invite.deleteMany.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Invitation has been deleted`),
});
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Unable to delete invitation. Please try again.`),
variant: 'destructive',
});
},
});
const onPaginationChange = (page: number, perPage: number) => {
updateSearchParams({
@@ -107,9 +104,7 @@ export const OrganisationMemberInvitesTable = () => {
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={row.original.email.slice(0, 1).toUpperCase()}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.email}</span>
}
primaryText={<span className="font-semibold text-foreground/80">{row.original.email}</span>}
/>
);
},
@@ -129,7 +124,7 @@ export const OrganisationMemberInvitesTable = () => {
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -149,17 +144,22 @@ export const OrganisationMemberInvitesTable = () => {
<Trans>Resend</Trans>
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () =>
deleteOrganisationMemberInvitations({
organisationId: organisation.id,
invitationIds: [row.original.id],
})
}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
{isOrganisationRoleWithinUserHierarchy(
organisation.currentOrganisationRole,
row.original.organisationRole,
) && (
<DropdownMenuItem
onClick={async () =>
deleteOrganisationMemberInvitations({
organisationId: organisation.id,
invitationIds: [row.original.id],
})
}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Remove</Trans>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
),
@@ -202,11 +202,7 @@ export const OrganisationMemberInvitesTable = () => {
),
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
}
{(table) => results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
);
};
@@ -1,12 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType } from '@prisma/client';
import { Edit, MoreHorizontal, Trash2 } from 'lucide-react';
import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { EXTENDED_ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
@@ -27,6 +18,13 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType } from '@prisma/client';
import { Edit, MoreHorizontal, Trash2 } from 'lucide-react';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { OrganisationMemberDeleteDialog } from '~/components/dialogs/organisation-member-delete-dialog';
import { OrganisationMemberUpdateDialog } from '~/components/dialogs/organisation-member-update-dialog';
@@ -79,9 +77,7 @@ export const OrganisationMembersDataTable = () => {
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={avatarFallbackText}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
}
primaryText={<span className="font-semibold text-foreground/80">{row.original.name}</span>}
secondaryText={row.original.email}
/>
);
@@ -102,15 +98,14 @@ export const OrganisationMembersDataTable = () => {
},
{
header: _(msg`Groups`),
cell: ({ row }) =>
row.original.groups.filter((group) => group.type === OrganisationGroupType.CUSTOM).length,
cell: ({ row }) => row.original.groups.filter((group) => group.type === OrganisationGroupType.CUSTOM).length,
},
{
header: _(msg`Actions`),
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -209,11 +204,7 @@ export const OrganisationMembersDataTable = () => {
),
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
}
{(table) => results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
);
};
@@ -1,11 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useSearchParams } from 'react-router';
import { Link } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@@ -19,6 +11,11 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router';
import { TeamDeleteDialog } from '../dialogs/team-delete-dialog';
@@ -63,9 +60,7 @@ export const OrganisationTeamsTable = () => {
avatarSrc={formatAvatarUrl(row.original.avatarImageId)}
avatarClass="h-12 w-12"
avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
}
primaryText={<span className="font-semibold text-foreground/80">{row.original.name}</span>}
secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/t/${row.original.url}`}
/>
</Link>
@@ -143,11 +138,7 @@ export const OrganisationTeamsTable = () => {
),
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
}
{(table) => results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
);
};
@@ -1,11 +1,3 @@
import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { trpc } from '@documenso/trpc/react';
@@ -19,6 +11,12 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useMemo, useState } from 'react';
import { ManagePublicTemplateDialog } from '~/components/dialogs/public-profile-template-manage-dialog';
@@ -74,7 +72,7 @@ export const SettingsPublicProfileTemplatesTable = () => {
return (
<div>
<div className="dark:divide-foreground/30 dark:border-foreground/30 mt-6 divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200">
<div className="mt-6 divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200 dark:divide-foreground/30 dark:border-foreground/30">
{/* Loading and error handling states. */}
{publicDirectTemplates.length === 0 && (
<>
@@ -82,12 +80,9 @@ export const SettingsPublicProfileTemplatesTable = () => {
Array(3)
.fill(0)
.map((_, index) => (
<div
key={index}
className="bg-background flex items-center justify-between gap-x-6 p-4"
>
<div key={index} className="flex items-center justify-between gap-x-6 bg-background p-4">
<div className="flex gap-x-2">
<FileIcon className="text-muted-foreground/40 h-8 w-8" strokeWidth={1.5} />
<FileIcon className="h-8 w-8 text-muted-foreground/40" strokeWidth={1.5} />
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
@@ -95,12 +90,12 @@ export const SettingsPublicProfileTemplatesTable = () => {
</div>
</div>
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
<MoreHorizontalIcon className="h-5 w-5 text-muted-foreground" />
</div>
))}
{isLoadingError && (
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm">
<div className="flex h-32 flex-col items-center justify-center text-muted-foreground text-sm">
<Trans>Unable to load your public profile templates at this time</Trans>
<button
onClick={(e) => {
@@ -114,12 +109,12 @@ export const SettingsPublicProfileTemplatesTable = () => {
)}
{!isLoading && (
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm">
<div className="flex h-32 flex-col items-center justify-center text-muted-foreground text-sm">
<Trans>No public profile templates found</Trans>
<ManagePublicTemplateDialog
directTemplates={privateDirectTemplates}
trigger={
<button className="hover:text-muted-foreground/80 mt-1 text-xs">
<button className="mt-1 text-xs hover:text-muted-foreground/80">
<Trans>Click here to get started</Trans>
</button>
}
@@ -131,25 +126,19 @@ export const SettingsPublicProfileTemplatesTable = () => {
{/* Public templates list. */}
{publicDirectTemplates.map((template) => (
<div
key={template.id}
className="bg-background flex items-center justify-between gap-x-6 p-4"
>
<div key={template.id} className="flex items-center justify-between gap-x-6 bg-background p-4">
<div className="flex gap-x-2">
<FileIcon
className="text-muted-foreground/40 h-8 w-8 flex-shrink-0"
strokeWidth={1.5}
/>
<FileIcon className="h-8 w-8 flex-shrink-0 text-muted-foreground/40" strokeWidth={1.5} />
<div>
<p className="text-sm">{template.publicTitle}</p>
<p className="text-xs text-neutral-400">{template.publicDescription}</p>
<p className="break-all text-sm">{template.publicTitle}</p>
<p className="break-all text-neutral-400 text-xs">{template.publicDescription}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
<MoreHorizontalIcon className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="center" side="left">
@@ -1,12 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { USER_SECURITY_AUDIT_LOG_MAP } from '@documenso/lib/constants/auth';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -16,6 +7,13 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { UAParser } from 'ua-parser-js';
const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT,
@@ -1,12 +1,3 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -20,16 +11,16 @@ import {
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export type SettingsSecurityPasskeyTableActionsProps = {
className?: string;
@@ -61,56 +52,47 @@ export const SettingsSecurityPasskeyTableActions = ({
},
});
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } =
trpc.auth.passkey.update.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Passkey has been updated`),
});
const { mutateAsync: updatePasskey, isPending: isUpdatingPasskey } = trpc.auth.passkey.update.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Passkey has been updated`),
});
setIsUpdateDialogOpen(false);
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(
msg`We are unable to update this passkey at the moment. Please try again later.`,
),
duration: 10000,
variant: 'destructive',
});
},
});
setIsUpdateDialogOpen(false);
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`We are unable to update this passkey at the moment. Please try again later.`),
duration: 10000,
variant: 'destructive',
});
},
});
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } =
trpc.auth.passkey.delete.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Passkey has been removed`),
});
const { mutateAsync: deletePasskey, isPending: isDeletingPasskey } = trpc.auth.passkey.delete.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Passkey has been removed`),
});
setIsDeleteDialogOpen(false);
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(
msg`We are unable to remove this passkey at the moment. Please try again later.`,
),
duration: 10000,
variant: 'destructive',
});
},
});
setIsDeleteDialogOpen(false);
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`We are unable to remove this passkey at the moment. Please try again later.`),
duration: 10000,
variant: 'destructive',
});
},
});
return (
<div className={cn('flex justify-end space-x-2', className)}>
<Dialog
open={isUpdateDialogOpen}
onOpenChange={(value) => !isUpdatingPasskey && setIsUpdateDialogOpen(value)}
>
<Dialog open={isUpdateDialogOpen} onOpenChange={(value) => !isUpdatingPasskey && setIsUpdateDialogOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
<Button variant="outline">
<Trans>Edit</Trans>
@@ -173,10 +155,7 @@ export const SettingsSecurityPasskeyTableActions = ({
</DialogContent>
</Dialog>
<Dialog
open={isDeleteDialogOpen}
onOpenChange={(value) => !isDeletingPasskey && setIsDeleteDialogOpen(value)}
>
<Dialog open={isDeleteDialogOpen} onOpenChange={(value) => !isDeletingPasskey && setIsDeleteDialogOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
<Button variant="destructive">
<Trans>Delete</Trans>
@@ -1,10 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
import { trpc } from '@documenso/trpc/react';
@@ -13,6 +6,11 @@ import { DataTable } from '@documenso/ui/primitives/data-table';
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router';
import { SettingsSecurityPasskeyTableActions } from './settings-security-passkey-table-actions';
@@ -66,9 +64,7 @@ export const SettingsSecurityPasskeyTable = () => {
header: _(msg`Last used`),
accessorKey: 'updatedAt',
cell: ({ row }) =>
row.original.lastUsedAt
? DateTime.fromJSDate(row.original.lastUsedAt).toRelative()
: _(msg`Never`),
row.original.lastUsedAt ? DateTime.fromJSDate(row.original.lastUsedAt).toRelative() : _(msg`Never`),
},
{
id: 'actions',
@@ -1,12 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType } from '@prisma/client';
import { EditIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { EXTENDED_TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations';
import { ZUrlSearchParamsSchema } from '@documenso/lib/types/search-params';
@@ -23,6 +14,13 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType } from '@prisma/client';
import { EditIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { useCurrentTeam } from '~/providers/team';
@@ -86,7 +84,7 @@ export const TeamGroupsTable = () => {
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
<MoreHorizontalIcon className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -99,10 +97,7 @@ export const TeamGroupsTable = () => {
teamGroupName={row.original.name ?? ''}
teamGroupRole={row.original.teamRole}
trigger={
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
title="Update team group role"
>
<DropdownMenuItem onSelect={(e) => e.preventDefault()} title="Update team group role">
<EditIcon className="mr-2 h-4 w-4" />
<Trans>Update role</Trans>
</DropdownMenuItem>
@@ -139,7 +134,7 @@ export const TeamGroupsTable = () => {
enable: isLoadingError,
}}
emptyState={
<div className="text-muted-foreground/60 flex h-60 flex-col items-center justify-center gap-y-4">
<div className="flex h-60 flex-col items-center justify-center gap-y-4 text-muted-foreground/60">
<p>
<Trans>No team groups found</Trans>
</p>
@@ -173,11 +168,7 @@ export const TeamGroupsTable = () => {
),
}}
>
{(table) =>
results.totalPages > 1 && (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
)
}
{(table) => results.totalPages > 1 && <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
);
};
@@ -1,12 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { EditIcon, MoreHorizontal, Trash2Icon } from 'lucide-react';
import { useSearchParams } from 'react-router';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { EXTENDED_TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations';
@@ -28,6 +19,13 @@ import {
} from '@documenso/ui/primitives/dropdown-menu';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
import { EditIcon, MoreHorizontal, Trash2Icon } from 'lucide-react';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { useCurrentTeam } from '~/providers/team';
@@ -100,9 +98,7 @@ export const TeamMembersTable = () => {
<AvatarWithText
avatarClass="h-12 w-12"
avatarFallback={avatarFallbackText}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
}
primaryText={<span className="font-semibold text-foreground/80">{row.original.name}</span>}
secondaryText={row.original.email}
/>
);
@@ -130,7 +126,7 @@ export const TeamMembersTable = () => {
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
@@ -231,9 +227,7 @@ export const TeamMembersTable = () => {
</DataTable>
<AnimateGenericFadeInOut key={groupQuery.isPending ? 'pending' : 'fetched'}>
{!groupQuery.isPending && (
<TeamInheritMemberAlert memberAccessTeamGroup={memberAccessTeamGroup || null} />
)}
{!groupQuery.isPending && <TeamInheritMemberAlert memberAccessTeamGroup={memberAccessTeamGroup || null} />}
</AnimateGenericFadeInOut>
</div>
);
@@ -1,11 +1,5 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import type { Recipient, TemplateDirectLink } from '@prisma/client';
import { Copy, Edit, FolderIcon, MoreHorizontal, Share2Icon, Trash2, Upload } from 'lucide-react';
import { Link } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { trpc as trpcReact } from '@documenso/trpc/react';
import {
DropdownMenu,
DropdownMenuContent,
@@ -13,11 +7,18 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, EnvelopeType, type TemplateDirectLink } from '@prisma/client';
import { Copy, Download, Edit, FolderIcon, MoreHorizontal, Pencil, Share2Icon, Trash2, Upload } from 'lucide-react';
import { useState } from 'react';
import { Link } from 'react-router';
import { EnvelopeDeleteDialog } from '../dialogs/envelope-delete-dialog';
import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog';
import { EnvelopeDuplicateDialog } from '../dialogs/envelope-duplicate-dialog';
import { EnvelopeRenameDialog } from '../dialogs/envelope-rename-dialog';
import { TemplateBulkSendDialog } from '../dialogs/template-bulk-send-dialog';
import { TemplateDeleteDialog } from '../dialogs/template-delete-dialog';
import { TemplateDirectLinkDialog } from '../dialogs/template-direct-link-dialog';
import { TemplateDuplicateDialog } from '../dialogs/template-duplicate-dialog';
import { TemplateMoveToFolderDialog } from '../dialogs/template-move-to-folder-dialog';
export type TemplatesTableActionDropdownProps = {
@@ -29,7 +30,7 @@ export type TemplatesTableActionDropdownProps = {
folderId?: string | null;
envelopeId: string;
directLink?: Pick<TemplateDirectLink, 'token' | 'enabled'> | null;
recipients: Recipient[];
recipients: TRecipientLite[];
};
templateRootPath: string;
teamId: number;
@@ -42,94 +43,116 @@ export const TemplatesTableActionDropdown = ({
teamId,
onDelete,
}: TemplatesTableActionDropdownProps) => {
const { user } = useSession();
const trpcUtils = trpcReact.useUtils();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
const [isRenameDialogOpen, setRenameDialogOpen] = useState(false);
const [isMoveToFolderDialogOpen, setMoveToFolderDialogOpen] = useState(false);
const isOwner = row.userId === user.id;
const isTeamTemplate = row.teamId === teamId;
const canMutate = isTeamTemplate;
const formatPath = `${templateRootPath}/${row.envelopeId}/edit`;
return (
<DropdownMenu>
<DropdownMenuTrigger data-testid="template-table-action-btn">
<MoreHorizontal className="text-muted-foreground h-5 w-5" />
<MoreHorizontal className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel>
<DropdownMenuItem disabled={!isOwner && !isTeamTemplate} asChild>
<Link to={formatPath}>
<Edit className="mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</Link>
</DropdownMenuItem>
<DropdownMenuItem
disabled={!isOwner && !isTeamTemplate}
onClick={() => setDuplicateDialogOpen(true)}
>
<Copy className="mr-2 h-4 w-4" />
<Trans>Duplicate</Trans>
</DropdownMenuItem>
<TemplateDirectLinkDialog
templateId={row.id}
recipients={row.recipients}
directLink={row.directLink}
<EnvelopeDownloadDialog
envelopeId={row.envelopeId}
envelopeStatus={DocumentStatus.DRAFT}
trigger={
<div
data-testid="template-direct-link"
className="hover:bg-accent hover:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors"
>
<Share2Icon className="mr-2 h-4 w-4" />
<Trans>Direct link</Trans>
</div>
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<Download className="mr-2 h-4 w-4" />
<Trans>Download</Trans>
</div>
</DropdownMenuItem>
}
/>
<DropdownMenuItem onClick={() => setMoveToFolderDialogOpen(true)}>
<FolderIcon className="mr-2 h-4 w-4" />
<Trans>Move to Folder</Trans>
</DropdownMenuItem>
{canMutate && (
<>
<DropdownMenuItem asChild>
<Link to={formatPath}>
<Edit className="mr-2 h-4 w-4" />
<Trans>Edit</Trans>
</Link>
</DropdownMenuItem>
<TemplateBulkSendDialog
templateId={row.id}
recipients={row.recipients}
trigger={
<div className="hover:bg-accent hover:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors">
<Upload className="mr-2 h-4 w-4" />
<Trans>Bulk Send via CSV</Trans>
</div>
}
/>
<DropdownMenuItem onClick={() => setRenameDialogOpen(true)}>
<Pencil className="mr-2 h-4 w-4" />
<Trans>Rename</Trans>
</DropdownMenuItem>
<DropdownMenuItem
disabled={!isOwner && !isTeamTemplate}
onClick={() => setDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
<EnvelopeDuplicateDialog
envelopeId={row.envelopeId}
envelopeType={EnvelopeType.TEMPLATE}
trigger={
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<Copy className="mr-2 h-4 w-4" />
<Trans>Duplicate</Trans>
</div>
</DropdownMenuItem>
}
/>
<TemplateDirectLinkDialog
templateId={row.id}
recipients={row.recipients}
directLink={row.directLink}
trigger={
<div
data-testid="template-direct-link"
className="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
>
<Share2Icon className="mr-2 h-4 w-4" />
<Trans>Direct link</Trans>
</div>
}
/>
<DropdownMenuItem onClick={() => setMoveToFolderDialogOpen(true)}>
<FolderIcon className="mr-2 h-4 w-4" />
<Trans>Move to Folder</Trans>
</DropdownMenuItem>
<TemplateBulkSendDialog
templateId={row.id}
recipients={row.recipients}
trigger={
<div className="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground">
<Upload className="mr-2 h-4 w-4" />
<Trans>Bulk Send via CSV</Trans>
</div>
}
/>
<EnvelopeDeleteDialog
id={row.envelopeId}
type={EnvelopeType.TEMPLATE}
status={DocumentStatus.DRAFT}
title={row.title}
canManageDocument={canMutate}
onDelete={onDelete}
trigger={
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
<div>
<Trash2 className="mr-2 h-4 w-4" />
<Trans>Delete</Trans>
</div>
</DropdownMenuItem>
}
/>
</>
)}
</DropdownMenuContent>
<TemplateDuplicateDialog
id={row.id}
open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen}
/>
<TemplateDeleteDialog
id={row.id}
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onDelete={onDelete}
/>
<TemplateMoveToFolderDialog
templateId={row.id}
templateTitle={row.title}
@@ -137,6 +160,17 @@ export const TemplatesTableActionDropdown = ({
onOpenChange={setMoveToFolderDialogOpen}
currentFolderId={row.folderId}
/>
<EnvelopeRenameDialog
id={row.envelopeId}
initialTitle={row.title}
open={isRenameDialogOpen}
onOpenChange={setRenameDialogOpen}
envelopeType="template"
onSuccess={async () => {
await trpcUtils.template.findTemplates.invalidate();
}}
/>
</DropdownMenu>
);
};
@@ -1,11 +1,3 @@
import { useMemo, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { AlertTriangle, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from 'lucide-react';
import { Link } from 'react-router';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
@@ -19,6 +11,12 @@ import { DataTablePagination } from '@documenso/ui/primitives/data-table-paginat
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { AlertTriangle, Building2Icon, Globe2Icon, InfoIcon, Link2Icon, Loader, LockIcon } from 'lucide-react';
import { useMemo, useTransition } from 'react';
import { Link } from 'react-router';
import { TemplateType } from '~/components/general/template/template-type';
import { useCurrentTeam } from '~/providers/team';
@@ -120,7 +118,7 @@ export const TemplatesTable = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="max-w-md space-y-2 !p-0 text-foreground">
<TooltipContent className="!p-0 max-w-md space-y-2 text-foreground">
<ul className="space-y-0.5 divide-y text-muted-foreground [&>li]:p-4">
<li>
<h2 className="mb-2 flex flex-row items-center font-semibold">
@@ -130,8 +128,8 @@ export const TemplatesTable = ({
<p>
<Trans>
Public templates are connected to your public profile. Any modifications to
public templates will also appear in your public profile.
Public templates are connected to your public profile. Any modifications to public templates
will also appear in your public profile.
</Trans>
</p>
</li>
@@ -143,9 +141,8 @@ export const TemplatesTable = ({
<p>
<Trans>
Direct link templates contain one dynamic recipient placeholder. Anyone with
access to this link can sign the document, and it will then appear on your
documents page.
Direct link templates contain one dynamic recipient placeholder. Anyone with access to this link
can sign the document, and it will then appear on your documents page.
</Trans>
</p>
</li>
@@ -157,34 +154,52 @@ export const TemplatesTable = ({
<p>
{team?.id ? (
<Trans>
Team only templates are not linked anywhere and are visible only to your
team.
</Trans>
<Trans>Team only templates are not linked anywhere and are visible only to your team.</Trans>
) : (
<Trans>Private templates can only be modified and viewed by you.</Trans>
)}
</p>
</li>
<li>
<h2 className="mb-2 flex flex-row items-center font-semibold">
<Building2Icon className="mr-2 h-5 w-5 text-orange-500 dark:text-orange-300" />
<Trans>Organisation</Trans>
</h2>
<p>
<Trans>
Organisation templates are shared across all teams within the same organisation. Only the owning
team can edit them.
</Trans>
</p>
</li>
</ul>
</TooltipContent>
</Tooltip>
</div>
),
accessorKey: 'type',
cell: ({ row }) => (
<div className="flex flex-row items-center">
<TemplateType type={row.original.type} />
cell: ({ row }) => {
const isFromOtherTeam = row.original.teamId !== team?.id;
{row.original.directLink?.token && (
<TemplateDirectLinkBadge
className="ml-2"
token={row.original.directLink.token}
enabled={row.original.directLink.enabled}
/>
)}
</div>
),
return (
<div className="flex flex-row items-center">
<TemplateType type={row.original.type} />
{isFromOtherTeam && row.original.team?.name && (
<span className="ml-2 text-muted-foreground text-xs">({row.original.team.name})</span>
)}
{row.original.directLink?.token && (
<TemplateDirectLinkBadge
className="ml-2"
token={row.original.directLink.token}
enabled={row.original.directLink.enabled}
/>
)}
</div>
);
},
},
{
header: _(msg`Actions`),
@@ -201,11 +216,7 @@ export const TemplatesTable = ({
documentRootPath={documentRootPath}
/>
<TemplatesTableActionDropdown
row={row.original}
teamId={team?.id}
templateRootPath={templateRootPath}
/>
<TemplatesTableActionDropdown row={row.original} teamId={team?.id} templateRootPath={templateRootPath} />
</div>
);
},
@@ -242,10 +253,7 @@ export const TemplatesTable = ({
<AlertDescription className="mt-2">
<Trans>
You have reached your document limit.{' '}
<Link
className="underline underline-offset-4"
to={`/o/${organisation.url}/settings/billing`}
>
<Link className="underline underline-offset-4" to={`/o/${organisation.url}/settings/billing`}>
Upgrade your account to continue!
</Link>
</Trans>
@@ -1,10 +1,3 @@
import { useMemo } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { SubscriptionStatus } from '@prisma/client';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
@@ -14,15 +7,18 @@ import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { Trans, useLingui } from '@lingui/react/macro';
import { SubscriptionStatus } from '@prisma/client';
import { useMemo } from 'react';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
export const UserBillingOrganisationsTable = () => {
const { t } = useLingui();
const { organisations } = useSession();
const billingOrganisations = useMemo(() => {
return organisations.filter((org) =>
canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole),
);
return organisations.filter((org) => canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole));
}, [organisations]);
const getSubscriptionStatusDisplay = (status: SubscriptionStatus | undefined) => {
@@ -56,9 +52,7 @@ export const UserBillingOrganisationsTable = () => {
avatarSrc={formatAvatarUrl(row.original.avatarImageId)}
avatarClass="h-12 w-12"
avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
primaryText={
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
}
primaryText={<span className="font-semibold text-foreground/80">{row.original.name}</span>}
secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/o/${row.original.url}`}
/>
</Link>
@@ -91,7 +85,7 @@ export const UserBillingOrganisationsTable = () => {
if (billingOrganisations.length === 0) {
return (
<div className="text-muted-foreground flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center text-muted-foreground">
<p className="text-sm">
<Trans>You don't manage billing for any organisations.</Trans>
</p>
@@ -1,10 +1,3 @@
import { useMemo } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
@@ -17,6 +10,11 @@ import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
import { DataTable } from '@documenso/ui/primitives/data-table';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { TableCell } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { useMemo } from 'react';
import { Link } from 'react-router';
import { OrganisationLeaveDialog } from '../dialogs/organisation-leave-dialog';
@@ -55,7 +53,7 @@ export const UserOrganisationsTable = () => {
avatarClass="h-12 w-12"
avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
primaryText={
<span className="text-foreground/80 font-semibold">
<span className="font-semibold text-foreground/80">
{isPersonalLayoutMode
? _(
msg({
@@ -92,10 +90,7 @@ export const UserOrganisationsTable = () => {
id: 'actions',
cell: ({ row }) => (
<div className="flex justify-end space-x-2">
{canExecuteOrganisationAction(
'MANAGE_ORGANISATION',
row.original.currentOrganisationRole,
) && (
{canExecuteOrganisationAction('MANAGE_ORGANISATION', row.original.currentOrganisationRole) && (
<Button variant="outline" asChild>
<Link to={`/o/${row.original.url}/settings`}>
<Trans>Manage</Trans>