chore: fix merge conflicts

This commit is contained in:
ephraimduncan
2025-12-29 20:02:51 +00:00
165 changed files with 6291 additions and 2774 deletions
@@ -156,8 +156,8 @@ export const AdminOrganisationMemberUpdateDialog = ({
<DialogDescription className="mt-4">
<Trans>
You are currently updating{' '}
<span className="font-bold">{organisationMemberName}.</span>
You are currently updating <span className="font-bold">{organisationMemberName}</span>
.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -353,8 +353,10 @@ export const EnvelopeDistributeDialog = ({
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Reply To Email</Trans>{' '}
<span className="text-muted-foreground">(Optional)</span>
<Trans>
Reply To Email{' '}
<span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
@@ -372,8 +374,10 @@ export const EnvelopeDistributeDialog = ({
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Subject</Trans>{' '}
<span className="text-muted-foreground">(Optional)</span>
<Trans>
Subject{' '}
<span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
@@ -390,8 +394,10 @@ export const EnvelopeDistributeDialog = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Message</Trans>{' '}
<span className="text-muted-foreground">(Optional)</span>
<Trans>
Message{' '}
<span className="text-muted-foreground">(Optional)</span>
</Trans>
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-2 h-4 w-4" />
@@ -117,7 +117,7 @@ export const EnvelopeItemDeleteDialog = ({
<DialogDescription className="mt-4">
<Trans>
You cannot delete this item because the document has been sent to recipients
You cannot delete this item because the document has been sent to recipients.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -121,7 +121,7 @@ export const OrganisationEmailDomainRecordContent = ({ records }: { records: Dom
<Trans>
Once you update your DNS records, it may take up to 48 hours for it to be propogated.
Once the DNS propagation is complete you will need to come back and press the "Sync"
domains button
domains button.
</Trans>
</AlertDescription>
</Alert>
@@ -148,8 +148,8 @@ export const OrganisationMemberUpdateDialog = ({
<DialogDescription className="mt-4">
<Trans>
You are currently updating{' '}
<span className="font-bold">{organisationMemberName}.</span>
You are currently updating <span className="font-bold">{organisationMemberName}</span>
.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -17,6 +17,7 @@ import { DocumentSigningDisclosure } from '../general/document-signing/document-
export type SignFieldSignatureDialogProps = {
initialSignature?: string;
fullName?: string;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
@@ -28,6 +29,7 @@ export const SignFieldSignatureDialog = createCallable<
>(
({
call,
fullName,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@@ -46,6 +48,7 @@ export const SignFieldSignatureDialog = createCallable<
</DialogHeader>
<SignaturePad
fullName={fullName}
value={localSignature ?? ''}
onChange={({ value }) => setLocalSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
@@ -8,6 +8,7 @@ import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
@@ -30,6 +31,13 @@ import {
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 type { Toast } from '@documenso/ui/primitives/use-toast';
import { useToast } from '@documenso/ui/primitives/use-toast';
@@ -53,26 +61,34 @@ export const TeamDeleteDialog = ({
const { toast } = useToast();
const { refreshSession } = useSession();
const currentOrganisation = useCurrentOrganisation();
const deleteMessage = _(msg`delete ${teamName}`);
const filteredTeams = currentOrganisation.teams.filter((team) => team.id !== teamId);
const ZDeleteTeamFormSchema = z.object({
teamName: z.literal(deleteMessage, {
errorMap: () => ({ message: _(msg`You must enter '${deleteMessage}' to proceed`) }),
}),
transferTeamId: z.string().optional(),
});
const form = useForm({
resolver: zodResolver(ZDeleteTeamFormSchema),
defaultValues: {
teamName: '',
transferTeamId: undefined,
},
});
const { mutateAsync: deleteTeam } = trpc.team.delete.useMutation();
const onFormSubmit = async () => {
const onFormSubmit = async (data: z.infer<typeof ZDeleteTeamFormSchema>) => {
try {
await deleteTeam({ teamId });
const transferTeamId = data.transferTeamId ? parseInt(data.transferTeamId, 10) : undefined;
await deleteTeam({ teamId, transferTeamId: transferTeamId || undefined });
await refreshSession();
@@ -168,6 +184,43 @@ export const TeamDeleteDialog = ({
)}
/>
{filteredTeams.length > 0 && (
<FormField
control={form.control}
name="transferTeamId"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Transfer documents to a different team</Trans>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue
placeholder={_(msg`Don't transfer (Delete all documents)`)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="-1">
<Trans>Don't transfer (Delete all documents)</Trans>
</SelectItem>
{filteredTeams.map((team) => (
<SelectItem key={team.id} value={team.id.toString()}>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
@@ -146,7 +146,7 @@ export const TeamMemberUpdateDialog = ({
<DialogDescription>
<Trans>
You are currently updating <span className="font-bold">{memberName}.</span>
You are currently updating <span className="font-bold">{memberName}</span>.
</Trans>
</DialogDescription>
</DialogHeader>
@@ -137,12 +137,12 @@ export const TemplateBulkSendDialog = ({
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
<div className="bg-muted/70 rounded-lg border p-4">
<div className="rounded-lg border bg-muted/70 p-4">
<h3 className="text-sm font-medium">
<Trans>CSV Structure</Trans>
</h3>
<p className="text-muted-foreground mt-1 text-sm">
<p className="mt-1 text-sm text-muted-foreground">
<Trans>
For each recipient, provide their email (required) and name (optional) in separate
columns. Download the template CSV below for the correct format.
@@ -153,7 +153,7 @@ export const TemplateBulkSendDialog = ({
<Trans>Current recipients:</Trans>
</p>
<ul className="text-muted-foreground mt-2 list-inside list-disc text-sm">
<ul className="mt-2 list-inside list-disc text-sm text-muted-foreground">
{recipients.map((recipient, index) => (
<li key={index}>
{recipient.name ? `${recipient.name} (${recipient.email})` : recipient.email}
@@ -167,7 +167,7 @@ export const TemplateBulkSendDialog = ({
<Trans>Download Template CSV</Trans>
</Button>
<p className="text-muted-foreground text-xs">
<p className="text-xs text-muted-foreground">
<Trans>Pre-formatted CSV template with example data.</Trans>
</p>
</div>
@@ -200,14 +200,14 @@ export const TemplateBulkSendDialog = ({
) : (
<div className="flex h-10 items-center rounded-md border px-3">
<div className="flex flex-1 items-center gap-2">
<FileIcon className="text-muted-foreground h-4 w-4" />
<FileIcon className="h-4 w-4 text-muted-foreground" />
<span className="flex-1 truncate text-sm">{value.name}</span>
</div>
<Button
type="button"
variant="link"
className="text-destructive hover:text-destructive p-0 text-xs"
className="p-0 text-xs text-destructive hover:text-destructive"
onClick={() => onChange(null)}
disabled={form.formState.isSubmitting}
>
@@ -220,9 +220,9 @@ export const TemplateBulkSendDialog = ({
)}
</FormControl>
{error && <p className="text-destructive text-sm">{error.message}</p>}
{error && <p className="text-sm text-destructive">{error.message}</p>}
<p className="text-muted-foreground text-xs">
<p className="text-xs text-muted-foreground">
<Trans>
Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use
template defaults.
@@ -247,7 +247,7 @@ export const TemplateBulkSendDialog = ({
<label
htmlFor="send-immediately"
className="text-muted-foreground ml-2 flex items-center text-sm"
className="ml-2 flex items-center text-sm text-muted-foreground"
>
<Trans>Send documents to recipients immediately</Trans>
</label>
@@ -1,136 +0,0 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { FilePlus, Loader } from 'lucide-react';
import { useNavigate } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
type TemplateCreateDialogProps = {
folderId?: string;
};
export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => {
const navigate = useNavigate();
const { user } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
const team = useCurrentTeam();
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
const [isUploadingFile, setIsUploadingFile] = useState(false);
const onFileDrop = async (files: File[]) => {
const file = files[0];
if (isUploadingFile) {
return;
}
setIsUploadingFile(true);
try {
const payload = {
title: file.name,
folderId: folderId,
} satisfies TCreateTemplatePayloadSchema;
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createTemplate(formData);
toast({
title: _(msg`Template document uploaded`),
description: _(
msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
),
duration: 5000,
});
setShowTemplateCreateDialog(false);
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
} catch {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Please try again later.`),
variant: 'destructive',
});
setIsUploadingFile(false);
}
};
return (
<Dialog
open={showTemplateCreateDialog}
onOpenChange={(value) => !isUploadingFile && setShowTemplateCreateDialog(value)}
>
<DialogTrigger asChild>
<Button className="cursor-pointer" disabled={!user.emailVerified}>
<FilePlus className="-ml-1 mr-2 h-4 w-4" />
<Trans>Template (Legacy)</Trans>
</Button>
</DialogTrigger>
<DialogContent className="w-full max-w-xl">
<DialogHeader>
<DialogTitle>
<Trans>New Template</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Templates allow you to quickly generate documents with pre-filled recipients and
fields.
</Trans>
</DialogDescription>
</DialogHeader>
<div className="relative">
<DocumentDropzone className="h-[40vh]" onDrop={onFileDrop} type="template" />
{isUploadingFile && (
<div className="bg-background/50 absolute inset-0 flex items-center justify-center rounded-lg">
<Loader className="text-muted-foreground h-12 w-12 animate-spin" />
</div>
)}
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary" disabled={isUploadingFile}>
<Trans>Close</Trans>
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -219,9 +219,8 @@ export const WebhookCreateDialog = ({ trigger, ...props }: WebhookCreateDialogPr
<FormDescription>
<Trans>
A secret that will be sent to your URL so you can verify that the request
has been sent by Documenso
has been sent by Documenso.
</Trans>
.
</FormDescription>
<FormMessage />
</FormItem>
@@ -438,6 +438,7 @@ export const EmbedDirectTemplateClientPage = ({
className="mt-2"
disabled={isThrottled || isSubmitting}
disableAnimation
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
@@ -455,6 +455,7 @@ export const EmbedSignDocumentV1ClientPage = ({
className="mt-2"
disabled={isThrottled || isSubmitting}
disableAnimation
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
@@ -319,6 +319,7 @@ export const MultiSignDocumentSigningView = ({
className="mt-2"
disabled={isSubmitting}
disableAnimation
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={
@@ -58,6 +58,7 @@ export type TDocumentPreferencesFormSchema = {
includeSigningCertificate: boolean | null;
includeAuditLog: boolean | null;
signatureTypes: DocumentSignatureType[];
delegateDocumentOwnership: boolean | null;
aiFeaturesEnabled: boolean | null;
};
@@ -73,6 +74,7 @@ type SettingsSubset = Pick<
| 'typedSignatureEnabled'
| 'uploadSignatureEnabled'
| 'drawSignatureEnabled'
| 'delegateDocumentOwnership'
| 'aiFeaturesEnabled'
>;
@@ -109,6 +111,7 @@ export const DocumentPreferencesForm = ({
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
message: msg`At least one signature type must be enabled`.id,
}),
delegateDocumentOwnership: z.boolean().nullable(),
aiFeaturesEnabled: z.boolean().nullable(),
});
@@ -125,6 +128,7 @@ export const DocumentPreferencesForm = ({
includeSigningCertificate: settings.includeSigningCertificate,
includeAuditLog: settings.includeAuditLog,
signatureTypes: extractTeamSignatureSettings({ ...settings }),
delegateDocumentOwnership: settings.delegateDocumentOwnership,
aiFeaturesEnabled: settings.aiFeaturesEnabled,
},
resolver: zodResolver(ZDocumentPreferencesFormSchema),
@@ -515,6 +519,52 @@ export const DocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="delegateDocumentOwnership"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Delegate Document Ownership</Trans>
</FormLabel>
<Select
{...field}
value={field.value === null ? '-1' : field.value.toString()}
onValueChange={(value) =>
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
}
>
<SelectTrigger className="bg-background text-muted-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">
<Trans>Yes</Trans>
</SelectItem>
<SelectItem value="false">
<Trans>No</Trans>
</SelectItem>
{canInherit && (
<SelectItem value={'-1'}>
<Trans>Inherit from organisation</Trans>
</SelectItem>
)}
</SelectContent>
</Select>
<FormDescription>
<Trans>
Enable team API tokens to delegate document ownership to another team member.
</Trans>
</FormDescription>
</FormItem>
)}
/>
{isAiFeaturesConfigured && (
<FormField
control={form.control}
+2 -1
View File
@@ -110,7 +110,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
<Label htmlFor="email" className="text-muted-foreground">
<Trans>Email</Trans>
</Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
<Input id="email" type="email" className="mt-2 bg-muted" value={user.email} disabled />
</div>
<FormField
@@ -124,6 +124,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
<FormControl>
<SignaturePadDialog
disabled={isSubmitting}
fullName={user.name ?? ''}
value={value}
onChange={(v) => onChange(v ?? '')}
/>
+10 -10
View File
@@ -244,11 +244,11 @@ export const SignInForm = ({
const errorMessage = match(error.code)
.with(
AuthenticationErrorCode.InvalidCredentials,
() => msg`The email or password provided is incorrect`,
() => msg`The email or password provided is incorrect.`,
)
.with(
AuthenticationErrorCode.InvalidTwoFactorCode,
() => msg`The two-factor authentication code provided is incorrect`,
() => msg`The two-factor authentication code provided is incorrect.`,
)
.otherwise(() => handleFallbackErrorMessages(error.code));
@@ -368,7 +368,7 @@ export const SignInForm = ({
<p className="mt-2 text-right">
<Link
to="/forgot-password"
className="text-muted-foreground text-sm duration-200 hover:opacity-70"
className="text-sm text-muted-foreground duration-200 hover:opacity-70"
>
<Trans>Forgot your password?</Trans>
</Link>
@@ -390,11 +390,11 @@ export const SignInForm = ({
<>
{hasSocialAuthEnabled && (
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">
<div className="h-px flex-1 bg-border" />
<span className="bg-transparent text-muted-foreground">
<Trans>Or continue with</Trans>
</span>
<div className="bg-border h-px flex-1" />
<div className="h-px flex-1 bg-border" />
</div>
)}
@@ -403,7 +403,7 @@ export const SignInForm = ({
type="button"
size="lg"
variant="outline"
className="bg-background text-muted-foreground border"
className="border bg-background text-muted-foreground"
disabled={isSubmitting}
onClick={onSignInWithGoogleClick}
>
@@ -417,7 +417,7 @@ export const SignInForm = ({
type="button"
size="lg"
variant="outline"
className="bg-background text-muted-foreground border"
className="border bg-background text-muted-foreground"
disabled={isSubmitting}
onClick={onSignInWithMicrosoftClick}
>
@@ -435,7 +435,7 @@ export const SignInForm = ({
type="button"
size="lg"
variant="outline"
className="bg-background text-muted-foreground border"
className="border bg-background text-muted-foreground"
disabled={isSubmitting}
onClick={onSignInWithOIDCClick}
>
@@ -452,7 +452,7 @@ export const SignInForm = ({
variant="outline"
disabled={isSubmitting}
loading={isPasskeyLoading}
className="bg-background text-muted-foreground border"
className="border bg-background text-muted-foreground"
onClick={onSignInWithPasskey}
>
{!isPasskeyLoading && <KeyRoundIcon className="-ml-1 mr-1 h-5 w-5" />}
+5 -5
View File
@@ -131,7 +131,7 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
const errorMessage = match(error.code)
.with(
AppErrorCode.UNAUTHORIZED,
() => msg`You do not have permission to create a token for this team`,
() => msg`You do not have permission to create a token for this team.`,
)
.otherwise(() => msg`Something went wrong. Please try again later.`);
@@ -212,12 +212,12 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
/>
<div>
<FormLabel className="text-muted-foreground mt-2">
<FormLabel className="mt-2 text-muted-foreground">
<Trans>Never expire</Trans>
</FormLabel>
<div className="block md:py-1.5">
<Switch
className="bg-background mt-2"
className="mt-2 bg-background"
checked={noExpirationDate}
onCheckedChange={setNoExpirationDate}
/>
@@ -254,14 +254,14 @@ export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
>
<Card gradient>
<CardContent className="p-4">
<p className="text-muted-foreground mt-2 text-sm">
<p className="mt-2 text-sm text-muted-foreground">
<Trans>
Your token was created successfully! Make sure to copy it because you won't be
able to see it again!
</Trans>
</p>
<p className="bg-muted-foreground/10 my-4 rounded-md px-2.5 py-1 font-mono text-sm">
<p className="my-4 rounded-md bg-muted-foreground/10 px-2.5 py-1 font-mono text-sm">
{newlyCreatedToken.token}
</p>
@@ -417,6 +417,7 @@ export const DirectTemplateSigningForm = ({
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
fullName={fullName}
value={signature ?? ''}
onChange={(value) => setSignature(value)}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
@@ -433,7 +434,7 @@ export const DirectTemplateSigningForm = ({
<div className="mt-4 flex gap-x-4">
<Button
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
className="w-full bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
size="lg"
variant="secondary"
disabled={isSubmitting}
@@ -280,6 +280,7 @@ export const DocumentSigningForm = ({
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
@@ -35,7 +35,7 @@ export const DocumentSigningMobileWidget = () => {
return (
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-center px-2 pb-2 sm:px-4 sm:pb-6">
<div className="pointer-events-auto w-full max-w-[760px]">
<div className="bg-card border-border overflow-hidden rounded-xl border shadow-2xl">
<div className="overflow-hidden rounded-xl border border-border bg-card shadow-2xl">
{/* Main Header Bar */}
<div className="flex items-center justify-between gap-4 p-4">
<div className="flex-1">
@@ -48,15 +48,15 @@ export const DocumentSigningMobileWidget = () => {
aria-label={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? (
<LucideChevronDown className="text-muted-foreground h-5 w-5 flex-shrink-0" />
<LucideChevronDown className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
) : (
<LucideChevronUp className="text-muted-foreground h-5 w-5 flex-shrink-0" />
<LucideChevronUp className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
)}
</Button>
)}
<div>
<h2 className="text-foreground text-lg font-semibold">
<h2 className="text-lg font-semibold text-foreground">
{match(recipient.role)
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
@@ -65,7 +65,7 @@ export const DocumentSigningMobileWidget = () => {
.otherwise(() => null)}
</h2>
<p className="text-muted-foreground -mt-0.5 text-sm">
<p className="-mt-0.5 text-sm text-muted-foreground">
{recipientFieldsRemaining.length === 0 ? (
match(recipient.role)
.with(RecipientRole.VIEWER, () => (
@@ -102,11 +102,11 @@ export const DocumentSigningMobileWidget = () => {
{recipient.role !== RecipientRole.VIEWER &&
recipient.role !== RecipientRole.ASSISTANT && (
<div className="px-4 pb-3">
<div className="bg-muted relative h-[4px] rounded-md">
<div className="relative h-[4px] rounded-md bg-muted">
<motion.div
layout="size"
layoutId="document-signing-mobile-widget-progress-bar"
className="bg-documenso absolute inset-y-0 left-0"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
@@ -117,11 +117,11 @@ export const DocumentSigningMobileWidget = () => {
{/* Expandable Content */}
{isExpanded && (
<div className="border-border animate-in slide-in-from-bottom-2 border-t p-4 duration-200">
<div className="border-t border-border p-4 duration-200 animate-in slide-in-from-bottom-2">
<EnvelopeSignerForm />
{!hidePoweredBy && (
<div className="bg-primary text-primary-foreground mt-2 inline-block rounded px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100 lg:hidden">
<div className="mt-2 inline-block rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground opacity-60 hover:opacity-100 lg:hidden">
<span>Powered by</span>
<BrandingLogo className="ml-2 inline-block h-[14px]" />
</div>
@@ -109,7 +109,7 @@ export const DocumentSigningPageViewV2 = () => {
<motion.div
layout="size"
layoutId="document-flow-container-step"
className="absolute inset-y-0 left-0 bg-documenso"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
@@ -56,8 +56,11 @@ export const DocumentSigningSignatureField = ({
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredDocumentSigningContext();
const {
fullName,
signature: providedSignature,
setSignature: setProvidedSignature,
} = useRequiredDocumentSigningContext();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
@@ -236,13 +239,13 @@ export const DocumentSigningSignatureField = ({
type="Signature"
>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-background">
<Loader className="h-5 w-5 animate-spin text-primary md:h-8 md:w-8" />
</div>
)}
{state === 'empty' && (
<p className="group-hover:text-primary font-signature text-muted-foreground group-hover:text-recipient-green text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200">
<p className="font-signature text-[clamp(0.575rem,25cqw,1.2rem)] text-xl text-muted-foreground duration-200 group-hover:text-primary group-hover:text-recipient-green">
<Trans>Signature</Trans>
</p>
)}
@@ -259,7 +262,7 @@ export const DocumentSigningSignatureField = ({
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
<p
ref={signatureRef}
className="font-signature text-muted-foreground w-full overflow-hidden break-all text-center leading-tight duration-200"
className="w-full overflow-hidden break-all text-center font-signature leading-tight text-muted-foreground duration-200"
style={{ fontSize: `${fontSize}rem` }}
>
{signature?.typedSignature}
@@ -272,12 +275,13 @@ export const DocumentSigningSignatureField = ({
<DialogTitle>
<Trans>
Sign as {recipient.name}{' '}
<div className="text-muted-foreground h-5">({recipient.email})</div>
<div className="h-5 text-muted-foreground">({recipient.email})</div>
</Trans>
</DialogTitle>
<SignaturePad
className="mt-2"
fullName={fullName}
value={localSignature ?? ''}
onChange={({ value }) => setLocalSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
@@ -14,9 +14,10 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { DocumentUploadButton as DocumentUploadButtonPrimitive } from '@documenso/ui/primitives/document-upload-button';
import {
@@ -31,9 +32,13 @@ import { useCurrentTeam } from '~/providers/team';
export type DocumentUploadButtonLegacyProps = {
className?: string;
type: EnvelopeType;
};
export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLegacyProps) => {
export const DocumentUploadButtonLegacy = ({
className,
type,
}: DocumentUploadButtonLegacyProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { user } = useSession();
@@ -54,8 +59,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync: createDocument } = trpc.document.create.useMutation();
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const disabledMessage = useMemo(() => {
if (!user.emailVerified) {
return msg`Verify your email to upload documents.`;
}
// No errors for templates.
if (type === EnvelopeType.TEMPLATE) {
return;
}
if (organisation.subscription && remaining.documents === 0) {
return msg`Document upload disabled due to unpaid invoices`;
}
@@ -64,11 +79,8 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
return msg`You have reached your document limit.`;
}
if (!user.emailVerified) {
return msg`Verify your email to upload documents.`;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [remaining.documents, user.emailVerified, team]);
}, [remaining.documents, user.emailVerified, team, type]);
const onFileDrop = async (file: File) => {
try {
@@ -80,44 +92,62 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
meta: {
timezone: userTimezone,
},
} satisfies TCreateDocumentPayloadSchema;
} satisfies TCreateDocumentPayloadSchema | TCreateTemplatePayloadSchema;
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createDocument(formData);
// Handle legacy document creation.
if (type === EnvelopeType.DOCUMENT) {
const { envelopeId: id } = await createDocument(formData);
void refreshLimits();
void refreshLimits();
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
await navigate(`${formatDocumentsPath(team.url)}/${id}/edit`);
toast({
title: _(msg`Document uploaded`),
description: _(msg`Your document has been uploaded successfully.`),
duration: 5000,
});
toast({
title: _(msg`Document uploaded`),
description: _(msg`Your document has been uploaded successfully.`),
duration: 5000,
});
analytics.capture('App: Document Uploaded', {
userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
analytics.capture('App: Document Uploaded', {
userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
}
// Handle legacy template creation.
if (type === EnvelopeType.TEMPLATE) {
const { envelopeId: id } = await createTemplate(formData);
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
toast({
title: _(msg`Template document uploaded`),
description: _(
msg`Your document has been uploaded successfully. You will be redirected to the template page.`,
),
duration: 5000,
});
}
} catch (err) {
const error = AppError.parseError(err);
console.error(err);
const errorMessage = match(error.code)
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs`)
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs.`)
.with(
AppErrorCode.LIMIT_EXCEEDED,
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
)
.with(
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
() => msg`You have reached the limit of the number of files per envelope`,
() => msg`You have reached the limit of the number of files per envelope.`,
)
.otherwise(() => msg`An error occurred while uploading your document.`);
@@ -149,17 +179,18 @@ export const DocumentUploadButtonLegacy = ({ className }: DocumentUploadButtonLe
<div>
<DocumentUploadButtonPrimitive
loading={isLoading}
disabled={remaining.documents === 0 || !user.emailVerified}
disabled={disabledMessage !== undefined}
disabledMessage={disabledMessage}
onDrop={async (files) => onFileDrop(files[0])}
onDropRejected={onFileDropRejected}
type={EnvelopeType.DOCUMENT}
type={type}
internalVersion="1"
/>
</div>
</TooltipTrigger>
{team?.id === undefined &&
type === EnvelopeType.DOCUMENT &&
remaining.documents > 0 &&
Number.isFinite(remaining.documents) && (
<TooltipContent>
@@ -339,7 +339,7 @@ export const EnvelopeEditorSettingsDialog = ({
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
{/* Sidebar. */}
<div className="bg-accent/20 flex w-80 flex-col border-r">
<div className="flex w-80 flex-col border-r bg-accent/20">
<DialogHeader className="p-6 pb-4">
<DialogTitle>Document Settings</DialogTitle>
</DialogHeader>
@@ -390,7 +390,7 @@ export const EnvelopeEditorSettingsDialog = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
<Trans>
Controls the language for the document, including the language
to be used for email notifications, and the final certificate
@@ -441,7 +441,7 @@ export const EnvelopeEditorSettingsDialog = ({
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
className="w-full bg-background"
emptySelectionPlaceholder="Select signature types"
/>
</FormControl>
@@ -518,7 +518,7 @@ export const EnvelopeEditorSettingsDialog = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add an external ID to the document. This can be used to identify
the document in external systems.
@@ -548,7 +548,7 @@ export const EnvelopeEditorSettingsDialog = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>
@@ -576,7 +576,7 @@ export const EnvelopeEditorSettingsDialog = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
<h2>
<strong>
<Trans>Document Distribution Method</Trans>
@@ -687,8 +687,10 @@ export const EnvelopeEditorSettingsDialog = ({
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Reply To Email</Trans>{' '}
<span className="text-muted-foreground">(Optional)</span>
<Trans>
Reply To Email{' '}
<span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
@@ -726,20 +728,21 @@ export const EnvelopeEditorSettingsDialog = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Message</Trans>{' '}
<span className="text-muted-foreground">(Optional)</span>
<Trans>
Message <span className="text-muted-foreground">(Optional)</span>
</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground p-4">
<TooltipContent className="p-4 text-muted-foreground">
<DocumentSendEmailMessageHelper />
</TooltipContent>
</Tooltip>
</FormLabel>
<FormControl>
<Textarea className="bg-background h-16 resize-none" {...field} />
<Textarea className="h-16 resize-none bg-background" {...field} />
</FormControl>
<FormMessage />
@@ -175,7 +175,7 @@ export default function EnvelopeEditor() {
<motion.div
layout="size"
layoutId="document-flow-container-step"
className="absolute inset-y-0 left-0 bg-documenso"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
}}
@@ -41,7 +41,7 @@ export default function EnvelopeSignerForm() {
if (recipient.role === RecipientRole.ASSISTANT) {
return (
<fieldset className="embed--DocumentWidgetForm dark:bg-background border-border rounded-2xl sm:border sm:p-3">
<fieldset className="embed--DocumentWidgetForm rounded-2xl border-border sm:border sm:p-3 dark:bg-background">
<RadioGroup
className="gap-0 space-y-2 shadow-none sm:space-y-3"
value={selectedAssistantRecipient?.id?.toString()}
@@ -54,7 +54,7 @@ export default function EnvelopeSignerForm() {
.map((r) => (
<div
key={r.id}
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
className="relative flex flex-col gap-4 rounded-lg border border-border bg-widget p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -69,15 +69,15 @@ export default function EnvelopeSignerForm() {
{r.name}
{r.id === recipient.id && (
<span className="text-muted-foreground ml-2">
<span className="ml-2 text-muted-foreground">
<Trans>(You)</Trans>
</span>
)}
</Label>
<p className="text-muted-foreground text-xs">{r.email}</p>
<p className="text-xs text-muted-foreground">{r.email}</p>
</div>
</div>
<div className="text-muted-foreground text-xs leading-[inherit]">
<div className="text-xs leading-[inherit] text-muted-foreground">
<Plural
value={assistantFields.filter((field) => field.recipientId === r.id).length}
one="# field"
@@ -103,7 +103,7 @@ export default function EnvelopeSignerForm() {
<Input
type="text"
id="full-name"
className="bg-background mt-2"
className="mt-2 bg-background"
value={fullName}
disabled={isNameLocked}
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
@@ -119,6 +119,7 @@ export default function EnvelopeSignerForm() {
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
@@ -374,6 +374,7 @@ export default function EnvelopeSignerPageRenderer() {
.with({ type: FieldType.SIGNATURE }, (field) => {
handleSignatureFieldClick({
field,
fullName,
signature,
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
@@ -123,14 +123,14 @@ export const EnvelopeDropZoneWrapper = ({
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
.with(
AppErrorCode.LIMIT_EXCEEDED,
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
)
.with(
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
() => t`You have reached the limit of the number of files per envelope`,
() => t`You have reached the limit of the number of files per envelope.`,
)
.otherwise(() => t`An error occurred during upload.`);
@@ -126,7 +126,7 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
console.error(err);
const errorMessage = match(error.code)
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs`)
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
.with(
'UNSUPPORTED_FILE_TYPE',
() => t`This file type is not supported. Please upload a PDF, DOCX, JPEG, or PNG file.`,
@@ -146,7 +146,7 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
)
.with(
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
() => t`You have reached the limit of the number of files per envelope`,
() => t`You have reached the limit of the number of files per envelope.`,
)
.otherwise(() => t`An error occurred while uploading your document.`);
@@ -15,7 +15,6 @@ import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
import { DocumentUploadButtonLegacy } from '~/components/general/document/document-upload-button-legacy';
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
import { useCurrentTeam } from '~/providers/team';
@@ -70,7 +69,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
<div>
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div
className="text-muted-foreground hover:text-muted-foreground/80 flex flex-1 items-center text-sm font-medium"
className="flex flex-1 items-center text-sm font-medium text-muted-foreground hover:text-muted-foreground/80"
data-testid="folder-grid-breadcrumbs"
>
<Link to={formatRootPath()} className="flex items-center">
@@ -100,10 +99,9 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
<div className="flex gap-4 sm:flex-row sm:justify-end">
<EnvelopeUploadButton type={type} folderId={parentId || undefined} />
{type === FolderType.DOCUMENT ? (
<DocumentUploadButtonLegacy /> // If you delete this, delete the component as well.
) : (
<TemplateCreateDialog folderId={parentId ?? undefined} /> // If you delete this, delete the component as well.
{/* If you delete this, delete the component as well. */}
{organisation.organisationClaim.flags.allowLegacyEnvelopes && (
<DocumentUploadButtonLegacy type={type} />
)}
<FolderCreateDialog type={type} />
@@ -113,7 +111,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
{isPending ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="border-border bg-card h-full rounded-lg border px-4 py-5">
<div key={index} className="h-full rounded-lg border border-border bg-card px-4 py-5">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded" />
<div className="flex w-full items-center justify-between">
@@ -194,7 +192,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
{foldersData.folders.length > 12 && (
<div className="mt-2 flex items-center justify-center">
<Link
className="text-muted-foreground hover:text-foreground text-sm font-medium"
className="text-sm font-medium text-muted-foreground hover:text-foreground"
to={formatViewAllFoldersPath()}
>
View all folders
@@ -2,6 +2,7 @@ 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';
@@ -82,7 +83,7 @@ export const AdminDashboardUsersTable = ({
<Button className="w-24" asChild>
<Link to={`/admin/users/${row.original.id}`}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
Edit
<Trans>Edit</Trans>
</Link>
</Button>
);
@@ -19,6 +19,7 @@ import { TableCell } from '@documenso/ui/primitives/table';
export type DocumentLogsTableProps = {
documentId: number;
userId?: number;
};
const dateFormat: DateTimeFormatOptions = {
@@ -26,7 +27,7 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12',
};
export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
export const DocumentLogsTable = ({ documentId, userId }: DocumentLogsTableProps) => {
const { _, i18n } = useLingui();
const [searchParams] = useSearchParams();
@@ -93,7 +94,9 @@ export const DocumentLogsTable = ({ documentId }: DocumentLogsTableProps) => {
{
header: _(msg`Action`),
accessorKey: 'type',
cell: ({ row }) => <span>{formatDocumentAuditLogAction(_, row.original).description}</span>,
cell: ({ row }) => (
<span>{formatDocumentAuditLogAction(_, row.original, userId).description}</span>
),
},
{
header: _(msg`IP Address`),
@@ -82,7 +82,9 @@ export const OrganisationGroupsDataTable = () => {
cell: ({ row }) => (
<div className="flex justify-end space-x-2">
<Button asChild variant="outline">
<Link to={`/o/${organisation.url}/settings/groups/${row.original.id}`}>Manage</Link>
<Link to={`/o/${organisation.url}/settings/groups/${row.original.id}`}>
<Trans>Manage</Trans>
</Link>
</Button>
<OrganisationGroupDeleteDialog