mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 18:04:55 +10:00
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:
@@ -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> (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>
|
||||
|
||||
Reference in New Issue
Block a user