Compare commits

..

25 Commits

Author SHA1 Message Date
191b333e34 chore: implement feedback 2025-07-25 11:41:46 +03:00
2c579c6455 chore: merge main 2025-07-25 10:09:39 +03:00
6bd688bde4 chore: implement feedback 2025-07-25 10:05:26 +03:00
c0a72123bd chore: implement feedback 2025-07-23 14:42:16 +03:00
d710f53fb5 chore: merged main 2025-07-23 14:03:30 +03:00
34caad2641 chore: document audit log 2025-07-07 16:10:41 +03:00
1511d2288c chore: visual changes 2025-07-07 15:28:52 +03:00
e19da93ce2 chore: template attachments 2025-07-07 13:31:55 +03:00
30b240cba2 chore: more feedback implementation 2025-07-07 12:21:07 +03:00
eb78706f35 chore: revert changes based on feedback 2025-07-07 12:04:20 +03:00
52b474d12b chore: implement feedback part 1
new form component added for document attachments with Zod validation and TRPC integration.
2025-07-04 16:29:57 +03:00
0b03bd3fce chore: remove unedeed file 2025-07-03 13:07:07 +03:00
15d0be17d7 chore: merge main 2025-07-03 12:47:37 +03:00
338965325d chore: merge main 2025-06-24 10:49:08 +03:00
3b476e9e1f chore: merged main 2025-05-07 11:17:15 +03:00
6da56887ee chore: simplify document attachment rendering in DocumentSigningForm
- Removed unnecessary Button wrapper around attachment links.
- Enhanced layout for attachment links with improved styling and structure.
2025-05-07 11:11:36 +03:00
cec25ac719 feat: add support for attachments in template management
- Enhanced TemplateEditForm to include attachments in the template data.
- Updated createDocumentFromTemplate to handle attachment creation.
- Modified updateTemplate to manage attachment updates and deletions.
- Integrated attachments into ZTemplateSchema and ZAddTemplateSettingsFormSchema for validation.
- Improved getTemplateById to fetch attachments alongside other template data.
2025-05-06 15:48:52 +03:00
d10ec437cf fix: improve document attachment rendering logic 2025-05-05 12:50:05 +03:00
dbacfaa841 feat: enhance document attachment updates and audit logging
- Implemented detailed handling for document attachment updates in DocumentHistorySheet.
- Updated updateDocument function to log changes only when attachments differ.
- Enhanced ZDocumentSchema to include attachment type validation.
- Refined audit log formatting for document attachment updates to improve clarity.
2025-05-01 11:39:07 +03:00
6980db57d3 feat: enhance document attachment handling and audit logging
- Added support for attachment updates in the updateDocument functionc.
- Introduced new audit log type for document attachments updates.
- Updated ZDocumentAuditLog schemas to include attachment-related events.
- Modified AddSettingsFormPartial to handle attachment IDs and types correctly.
- Set default value for attachment type in the Prisma schema.
2025-04-30 15:53:58 +03:00
e3f8e76e6a feat: enhance document schema and update attachment handling
- Added attachments support to ZCreateDocumentMutationSchema and ZUpdateDocumentRequestSchema.
- Updated ZDocumentSchema to validate attachments with specific fields.
- Modified updateDocument function to handle attachment creation and deletion.
- Enhanced AddSettingsFormSchema to include attachments with proper validation.
2025-04-29 15:14:58 +03:00
396a7db587 feat: enhance document management by adding attachments support
- Updated DocumentEditForm to include attachments in the document data.
- Modified getDocumentWithDetailsById to fetch attachments.
- Updated ZDocumentSchema to validate attachments.
- Enhanced AddSettingsFormPartial to handle attachments with default values and updated field names.
2025-04-29 14:11:11 +03:00
7ac48cb3f5 chore: add template attachment management feature 2025-04-25 14:25:33 +03:00
f7ee4d0ba2 chore: merged main 2025-04-25 13:58:55 +03:00
1b67be9099 feat: add document attachments feature 2025-04-25 13:49:22 +03:00
182 changed files with 2503 additions and 58844 deletions

View File

@ -136,5 +136,3 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
NEXT_PRIVATE_LOGGER_FILE_PATH=
# [[PLAIN SUPPORT]]
NEXT_PRIVATE_PLAIN_API_KEY=

View File

@ -20,6 +20,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PAT }}
- uses: ./.github/actions/node-install

View File

@ -5,14 +5,15 @@ import { Callout, Steps } from 'nextra/components';
Email Domains allow you to send emails to recipients from your own domain instead of the default Documenso email address.
<Callout type="info">
**Enterprise Only**: Email Domains is only available to Enterprise customers and custom plans
**Platform and Enterprise Only**: Email Domains is only available to Platform and Enterprise
customers.
</Callout>
## Creating Email Domains
Before setting up email domains, ensure you have:
- An Enterprise subscription
- A Platform or Enterprise subscription
- Access to your domain's DNS settings
- Access to your Documenso organisation as an admin or manager

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@ -96,17 +96,16 @@ export const AdminOrganisationCreateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
useEffect(() => {
form.reset();
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">

View File

@ -1,159 +0,0 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { User } from '@prisma/client';
import { useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type AdminUserResetTwoFactorDialogProps = {
className?: string;
user: User;
};
export const AdminUserResetTwoFactorDialog = ({
className,
user,
}: AdminUserResetTwoFactorDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const [email, setEmail] = useState('');
const [open, setOpen] = useState(false);
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } =
trpc.admin.user.resetTwoFactor.useMutation();
const onResetTwoFactor = async () => {
try {
await resetTwoFactor({
userId: user.id,
});
toast({
title: _(msg`2FA Reset`),
description: _(msg`The user's two factor authentication has been reset successfully.`),
duration: 5000,
});
await revalidate();
setOpen(false);
} catch (err) {
const error = AppError.parseError(err);
const errorMessage = match(error.code)
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
.with(
AppErrorCode.UNAUTHORIZED,
() => msg`You are not authorized to reset two factor authentcation for this user.`,
)
.otherwise(
() => msg`An error occurred while resetting two factor authentication for the user.`,
);
toast({
title: _(msg`Error`),
description: _(errorMessage),
variant: 'destructive',
duration: 7500,
});
}
};
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (!newOpen) {
setEmail('');
}
};
return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>Reset Two Factor Authentication</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Reset the users two factor authentication. This action is irreversible and will
disable two factor authentication for the user.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="destructive">
<Trans>Reset 2FA</Trans>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>
<Trans>Reset Two Factor Authentication</Trans>
</DialogTitle>
</DialogHeader>
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
<Trans>
This action is irreversible. Please ensure you have informed the user before
proceeding.
</Trans>
</AlertDescription>
</Alert>
<div>
<DialogDescription>
<Trans>
To confirm, please enter the accounts email address <br />({user.email}).
</Trans>
</DialogDescription>
<Input
className="mt-2"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<DialogFooter>
<Button
variant="destructive"
disabled={email !== user.email}
onClick={onResetTwoFactor}
loading={isResettingTwoFactor}
>
<Trans>Reset 2FA</Trans>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@ -73,23 +73,20 @@ export const DocumentDeleteDialog = ({
},
});
useEffect(() => {
if (open) {
setInputValue('');
setIsDeleteEnabled(status === DocumentStatus.DRAFT);
}
}, [open, status]);
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
setIsDeleteEnabled(event.target.value === _(deleteMessage));
};
const handleOpenChange = (value: boolean) => {
if (value) {
setInputValue('');
setIsDeleteEnabled(status === DocumentStatus.DRAFT);
}
if (!isPending) {
onOpenChange(value);
}
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !isPending && onOpenChange(value)}>
<DialogContent>
<DialogHeader>
<DialogTitle>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -83,6 +83,15 @@ export const DocumentMoveToFolderDialog = ({
const { mutateAsync: moveDocumentToFolder } = trpc.folder.moveDocumentToFolder.useMutation();
useEffect(() => {
if (!open) {
form.reset();
setSearchTerm('');
} else {
form.reset({ folderId: currentFolderId });
}
}, [open, currentFolderId, form]);
const onSubmit = async (data: TMoveDocumentFormSchema) => {
try {
await moveDocumentToFolder({
@ -136,22 +145,12 @@ export const DocumentMoveToFolderDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
setSearchTerm('');
} else {
form.reset({ folderId: currentFolderId });
}
onOpenChange(value);
};
const filteredFolders = folders?.data.filter((folder) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog {...props} open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
@ -80,15 +80,14 @@ export const FolderCreateDialog = ({ type, trigger, ...props }: FolderCreateDial
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!isCreateFolderOpen) {
form.reset();
}
setIsCreateFolderOpen(value);
};
}, [isCreateFolderOpen, form]);
return (
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={handleOpenChange}>
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
<DialogTrigger asChild>
{trigger ?? (
<Button

View File

@ -1,3 +1,5 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@ -90,15 +92,14 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!isOpen) {
form.reset();
}
onOpenChange(value);
};
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@ -97,13 +97,12 @@ export const FolderMoveDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!isOpen) {
form.reset();
setSearchTerm('');
}
onOpenChange(value);
};
}, [isOpen, form]);
// Filter out the current folder, only show folders of the same type, and filter by search term
const filteredFolders = foldersData?.filter(
@ -114,7 +113,7 @@ export const FolderMoveDialog = ({
);
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>

View File

@ -1,3 +1,5 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@ -69,6 +71,15 @@ export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdat
},
});
useEffect(() => {
if (folder) {
form.reset({
name: folder.name,
visibility: folder.visibility ?? DocumentVisibility.EVERYONE,
});
}
}, [folder, form]);
const onFormSubmit = async (data: TUpdateFolderFormSchema) => {
if (!folder) {
return;
@ -99,18 +110,8 @@ export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdat
}
};
const handleOpenChange = (value: boolean) => {
if (value && folder) {
form.reset({
name: folder.name,
visibility: folder.visibility ?? DocumentVisibility.EVERYONE,
});
}
onOpenChange(value);
};
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>

View File

@ -76,7 +76,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
const [selectedPriceId, setSelectedPriceId] = useState<string>('');
const [open, setOpen] = useState(actionSearchParam === 'add-organisation');
const [open, setOpen] = useState(false);
const form = useForm({
resolver: zodResolver(ZCreateOrganisationFormSchema),
@ -91,19 +91,6 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
enabled: IS_BILLING_ENABLED(),
});
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
if (actionSearchParam === 'add-organisation') {
updateSearchParams({ action: null });
}
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
const onFormSubmit = async ({ name }: TCreateOrganisationFormSchema) => {
try {
const response = await createOrganisation({
@ -139,6 +126,17 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
}
};
useEffect(() => {
if (actionSearchParam === 'add-organisation') {
setOpen(true);
updateSearchParams({ action: null });
}
}, [actionSearchParam, open]);
useEffect(() => {
form.reset();
}, [open, form]);
const isIndividualPlan = (priceId: string) => {
return (
plansData?.plans[INTERNAL_CLAIM_ID.INDIVIDUAL]?.monthlyPrice?.id === priceId ||
@ -147,7 +145,11 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
};
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@ -312,16 +314,13 @@ const BillingPlanForm = ({
};
},
);
}, [plans, t]);
}, [plans]);
useEffect(() => {
if (value === '' && !canCreateFreeOrganisation && dynamicPlans.length > 0) {
const defaultValue = dynamicPlans[0][billingPeriod]?.id ?? '';
if (defaultValue) {
onChange(defaultValue);
}
if (value === '' && !canCreateFreeOrganisation) {
onChange(dynamicPlans[0][billingPeriod]?.id ?? '');
}
}, [canCreateFreeOrganisation, dynamicPlans, billingPeriod, onChange, value]);
}, [value]);
const onBillingPeriodChange = (billingPeriod: 'monthlyPrice' | 'yearlyPrice') => {
const plan = dynamicPlans.find(

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -93,19 +93,14 @@ export const OrganisationDeleteDialog = ({ trigger }: OrganisationDeleteDialogPr
}
};
const handleOpenChange = (value: boolean) => {
if (form.formState.isSubmitting) {
return;
}
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
}
setOpen(value);
};
}, [open, form]);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="destructive">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@ -73,6 +73,13 @@ export const OrganisationEmailCreateDialog = ({
const { mutateAsync: createOrganisationEmail, isPending } =
trpc.enterprise.organisation.email.create.useMutation();
// Reset state when dialog closes
useEffect(() => {
if (!open) {
form.reset();
}
}, [open, form]);
const onFormSubmit = async (data: TCreateOrganisationEmailFormSchema) => {
try {
await createOrganisationEmail({
@ -107,17 +114,8 @@ export const OrganisationEmailCreateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
if (!isPending) {
setOpen(value);
}
};
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog {...props} open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">

View File

@ -36,12 +36,6 @@ export const OrganisationEmailDeleteDialog = ({
const organisation = useCurrentOrganisation();
const handleOpenChange = (value: boolean) => {
if (!isDeleting) {
setOpen(value);
}
};
const { mutateAsync: deleteEmail, isPending: isDeleting } =
trpc.enterprise.organisation.email.delete.useMutation({
onSuccess: () => {
@ -64,7 +58,7 @@ export const OrganisationEmailDeleteDialog = ({
});
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="secondary">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@ -75,6 +75,14 @@ export const OrganisationEmailDomainCreateDialog = ({
const { mutateAsync: createOrganisationEmail } =
trpc.enterprise.organisation.emailDomain.create.useMutation();
// Reset state when dialog closes
useEffect(() => {
if (!open) {
form.reset();
setStep('domain');
}
}, [open, form]);
const onFormSubmit = async ({ domain }: TCreateOrganisationEmailDomainFormSchema) => {
try {
const { records } = await createOrganisationEmail({
@ -110,18 +118,12 @@ export const OrganisationEmailDomainCreateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
setStep('domain');
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@ -87,20 +87,23 @@ export const OrganisationEmailUpdateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (value) {
form.reset({
emailName: organisationEmail.emailName,
// replyTo: organisationEmail.replyTo ?? undefined,
});
useEffect(() => {
if (!open) {
return;
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
form.reset({
emailName: organisationEmail.emailName,
// replyTo: organisationEmail.replyTo ?? undefined,
});
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger}
</DialogTrigger>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@ -117,17 +117,16 @@ export const OrganisationGroupCreateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
useEffect(() => {
form.reset();
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">

View File

@ -1,4 +1,4 @@
import { useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -193,6 +193,13 @@ export const OrganisationMemberInviteDialog = ({
return 'form';
}, [fullOrganisation]);
useEffect(() => {
if (!open) {
form.reset();
setInvitationType('INDIVIDUAL');
}
}, [open, form]);
const onFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files?.length) {
return;
@ -260,18 +267,12 @@ export const OrganisationMemberInviteDialog = ({
});
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
setInvitationType('INDIVIDUAL');
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -106,27 +106,32 @@ export const OrganisationMemberUpdateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (value) {
form.reset();
if (
!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, organisationMemberRole)
) {
setOpen(false);
toast({
title: _(msg`You cannot modify a organisation member who has a higher role than you.`),
variant: 'destructive',
});
return;
}
useEffect(() => {
if (!open) {
return;
}
if (!form.formState.isSubmitting) {
setOpen(value);
form.reset();
if (
!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, organisationMemberRole)
) {
setOpen(false);
toast({
title: _(msg`You cannot modify a organisation member who has a higher role than you.`),
variant: 'destructive',
});
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, currentUserOrganisationRole, organisationMemberRole, form, toast]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -120,21 +120,24 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
return passkeyName;
};
const handleDialogOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!open) {
const defaultPasskeyName = extractDefaultPasskeyName();
form.reset({
passkeyName: defaultPasskeyName,
});
setFormError(null);
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleDialogOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button variant="secondary" loading={isPending}>

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -66,14 +66,14 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
const updateSearchParams = useUpdateSearchParams();
const organisation = useCurrentOrganisation();
const actionSearchParam = searchParams?.get('action');
const shouldOpenDialog = actionSearchParam === 'add-team';
const [open, setOpen] = useState(shouldOpenDialog);
const [open, setOpen] = useState(false);
const { data: fullOrganisation } = trpc.organisation.get.useQuery({
organisationReference: organisation.id,
});
const actionSearchParam = searchParams?.get('action');
const form = useForm({
resolver: zodResolver(ZCreateTeamFormSchema),
defaultValues: {
@ -85,18 +85,6 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
const { mutateAsync: createTeam } = trpc.team.create.useMutation();
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
if (shouldOpenDialog) {
updateSearchParams({ action: null });
}
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
const onFormSubmit = async ({ teamName, teamUrl, inheritMembers }: TCreateTeamFormSchema) => {
try {
await createTeam({
@ -162,8 +150,23 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
return 'form';
}, [fullOrganisation]);
useEffect(() => {
if (actionSearchParam === 'add-team') {
setOpen(true);
updateSearchParams({ action: null });
}
}, [actionSearchParam, open]);
useEffect(() => {
form.reset();
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -114,17 +114,14 @@ export const TeamDeleteDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
}, [open, form]);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="destructive">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -103,17 +103,18 @@ export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDi
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button variant="outline" loading={isPending} className="bg-background">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -92,17 +92,18 @@ export const TeamEmailUpdateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="outline" className="bg-background">

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
@ -107,7 +107,7 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
duration: 5000,
});
handleClose();
setOpen(false);
} catch {
toast({
title: t`An unknown error occurred`,
@ -117,23 +117,17 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
}
};
const handleClose = () => {
setOpen(false);
form.reset();
setStep('SELECT');
};
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
handleClose();
useEffect(() => {
if (!open) {
form.reset();
setStep('SELECT');
}
};
}, [open, form]);
return (
<Dialog
{...props}
open={open}
onOpenChange={handleOpenChange}
// Disable automatic onOpenChange events to prevent dialog from closing if auser 'accidentally' clicks the overlay.
// Since it would be annoying to redo the whole process.
>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -106,17 +106,22 @@ export const TeamGroupUpdateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!form.formState.isSubmitting) {
if (value) {
form.reset();
}
setOpen(value);
useEffect(() => {
if (!open) {
return;
}
};
form.reset();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, team.currentTeamRole, teamGroupRole, form, toast]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">

View File

@ -1,12 +1,10 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { TeamMemberRole } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { z } from 'zod';
@ -41,7 +39,6 @@ import {
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
@ -119,17 +116,20 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
setStep('SELECT');
}
// Disable automatic onOpenChange events to prevent dialog from closing if user 'accidentally' clicks the overlay.
// Since it would be annoying to redo the whole process, we handle open state manually
};
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
// Disable automatic onOpenChange events to prevent dialog from closing if auser 'accidentally' clicks the overlay.
// Since it would be annoying to redo the whole process.
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
<Button variant="secondary" onClick={() => setOpen(true)}>
<Trans>Add members</Trans>
@ -140,28 +140,8 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
{match(step)
.with('SELECT', () => (
<DialogHeader>
<DialogTitle className="flex flex-row items-center">
<DialogTitle>
<Trans>Add members</Trans>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-xs">
<Trans>
To be able to add members to a team, you must first add them to the
organisation. For more information, please see the{' '}
<Link
to="https://docs.documenso.com/users/organisations/members"
target="_blank"
rel="noreferrer"
className="text-documenso-700 hover:text-documenso-600 hover:underline"
>
documentation
</Link>
.
</Trans>
</TooltipContent>
</Tooltip>
</DialogTitle>
<DialogDescription>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -106,25 +106,30 @@ export const TeamMemberUpdateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (value) {
form.reset();
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, memberTeamRole)) {
setOpen(false);
toast({
title: _(msg`You cannot modify a team member who has a higher role than you.`),
variant: 'destructive',
});
return;
}
useEffect(() => {
if (!open) {
return;
}
if (!form.formState.isSubmitting) {
setOpen(value);
form.reset();
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, memberTeamRole)) {
setOpen(false);
toast({
title: _(msg`You cannot modify a team member who has a higher role than you.`),
variant: 'destructive',
});
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, currentUserTeamRole, memberTeamRole, form, toast]);
return (
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@ -186,20 +186,16 @@ export const TemplateDirectLinkDialog = ({
const isLoading =
isCreatingTemplateDirectLink || isTogglingTemplateAccess || isDeletingTemplateDirectLink;
const handleOpenChange = (value: boolean) => {
if (!isLoading) {
if (value) {
resetCreateTemplateDirectLink();
setCurrentStep(token ? 'MANAGE' : 'ONBOARD');
setSelectedRecipientId(null);
}
useEffect(() => {
resetCreateTemplateDirectLink();
setCurrentStep(token ? 'MANAGE' : 'ONBOARD');
setSelectedRecipientId(null);
onOpenChange(value);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
<fieldset disabled={isLoading} className="relative">
<AnimateGenericFadeInOut motionKey={currentStep}>
{match({ token, currentStep })

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -85,6 +85,15 @@ export function TemplateMoveToFolderDialog({
const { mutateAsync: moveTemplateToFolder } = trpc.folder.moveTemplateToFolder.useMutation();
useEffect(() => {
if (!isOpen) {
form.reset();
setSearchTerm('');
} else {
form.reset({ folderId: currentFolderId ?? null });
}
}, [isOpen, currentFolderId, form]);
const onSubmit = async (data: TMoveTemplateFormSchema) => {
try {
await moveTemplateToFolder({
@ -128,22 +137,12 @@ export function TemplateMoveToFolderDialog({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
setSearchTerm('');
} else {
form.reset({ folderId: currentFolderId ?? null });
}
onOpenChange(value);
};
const filteredFolders = folders?.data?.filter((folder) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
<Dialog {...props} open={isOpen} onOpenChange={handleOpenChange}>
<Dialog {...props} open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -15,7 +15,6 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import {
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
isTemplateRecipientEmailPlaceholder,
} from '@documenso/lib/constants/template';
import { AppError } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
@ -203,17 +202,14 @@ export function TemplateUseDialog({
name: 'recipients',
});
const handleOpenChange = (value: boolean) => {
if (form.formState.isSubmitting) return;
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
}
setOpen(value);
};
}, [open, form]);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>
{trigger || (
<Button variant="outline" className="bg-background">
@ -283,11 +279,7 @@ export function TemplateUseDialog({
<FormControl>
<Input
{...field}
placeholder={
isTemplateRecipientEmailPlaceholder(field.value)
? ''
: _(msg`Email`)
}
placeholder={recipients[index].email || _(msg`Email`)}
/>
</FormControl>
<FormMessage />
@ -492,7 +484,6 @@ export function TemplateUseDialog({
<input
type="file"
data-testid="template-use-dialog-file-input"
className="absolute h-full w-full opacity-0"
accept=".pdf,application/pdf"
onChange={(e) => {

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -95,17 +95,17 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!isOpen) {
form.reset();
}
if (!form.formState.isSubmitting) {
setIsOpen(value);
}
};
}, [isOpen, form]);
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<Dialog
open={isOpen}
onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}
>
<DialogTrigger asChild={true}>
{children ?? (
<Button className="mr-4" variant="destructive">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -88,17 +88,14 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
useEffect(() => {
if (!open) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
}, [open, form]);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>
{children ?? (
<Button className="mr-4" variant="destructive">

View File

@ -1,10 +1,12 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro';
export const EmbedDocumentWaitingForTurn = () => {
const [hasPostedMessage, setHasPostedMessage] = useState(false);
useEffect(() => {
if (window.parent) {
if (window.parent && !hasPostedMessage) {
window.parent.postMessage(
{
action: 'document-waiting-for-turn',
@ -13,7 +15,13 @@ export const EmbedDocumentWaitingForTurn = () => {
'*',
);
}
}, []);
setHasPostedMessage(true);
}, [hasPostedMessage]);
if (!hasPostedMessage) {
return null;
}
return (
<div className="embed--WaitingForTurn relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@ -136,19 +136,18 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
setIsOpen(true);
};
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
useEffect(() => {
enable2FAForm.reset();
if (!open) {
enable2FAForm.reset();
if (recoveryCodes && recoveryCodes.length > 0) {
setRecoveryCodes(null);
}
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
setRecoveryCodes(null);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild={true}>
<Button
className="flex-shrink-0"

View File

@ -55,7 +55,6 @@ export type TDocumentPreferencesFormSchema = {
documentDateFormat: TDocumentMetaDateFormat | null;
includeSenderDetails: boolean | null;
includeSigningCertificate: boolean | null;
includeAuditLog: boolean | null;
signatureTypes: DocumentSignatureType[];
};
@ -67,7 +66,6 @@ type SettingsSubset = Pick<
| 'documentDateFormat'
| 'includeSenderDetails'
| 'includeSigningCertificate'
| 'includeAuditLog'
| 'typedSignatureEnabled'
| 'uploadSignatureEnabled'
| 'drawSignatureEnabled'
@ -98,7 +96,6 @@ export const DocumentPreferencesForm = ({
documentDateFormat: ZDocumentMetaTimezoneSchema.nullable(),
includeSenderDetails: z.boolean().nullable(),
includeSigningCertificate: z.boolean().nullable(),
includeAuditLog: z.boolean().nullable(),
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
message: msg`At least one signature type must be enabled`.id,
}),
@ -115,7 +112,6 @@ export const DocumentPreferencesForm = ({
documentDateFormat: settings.documentDateFormat as TDocumentMetaDateFormat | null,
includeSenderDetails: settings.includeSenderDetails,
includeSigningCertificate: settings.includeSigningCertificate,
includeAuditLog: settings.includeAuditLog,
signatureTypes: extractTeamSignatureSettings({ ...settings }),
},
resolver: zodResolver(ZDocumentPreferencesFormSchema),
@ -456,56 +452,6 @@ export const DocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="includeAuditLog"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Include the Audit Logs in the Document</Trans>
</FormLabel>
<FormControl>
<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>
</FormControl>
<FormDescription>
<Trans>
Controls whether the audit logs will be included in the document when it is
downloaded. The audit logs can still be downloaded from the logs page
separately.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update</Trans>

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
@ -116,18 +116,9 @@ export const SignInForm = ({
const { mutateAsync: createPasskeySigninOptions } =
trpc.auth.createPasskeySigninOptions.useMutation();
const emailFromHash = useMemo(() => {
if (typeof window === 'undefined') {
return null;
}
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
return params.get('email');
}, []);
const form = useForm<TSignInFormSchema>({
values: {
email: emailFromHash ?? initialEmail ?? '',
email: initialEmail ?? '',
password: '',
totpCode: '',
backupCode: '',
@ -296,6 +287,18 @@ export const SignInForm = ({
}
};
useEffect(() => {
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
const email = params.get('email');
if (email) {
form.setValue('email', email);
}
}, [form]);
return (
<Form {...form}>
<form

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
@ -84,19 +84,10 @@ export const SignUpForm = ({
const utmSrc = searchParams.get('utm_source') ?? null;
const emailFromHash = useMemo(() => {
if (typeof window === 'undefined') {
return null;
}
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
return params.get('email');
}, []);
const form = useForm<TSignUpFormSchema>({
values: {
name: '',
email: emailFromHash ?? initialEmail ?? '',
email: initialEmail ?? '',
password: '',
signature: '',
},
@ -171,6 +162,18 @@ export const SignUpForm = ({
}
};
useEffect(() => {
const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
const email = params.get('email');
if (email) {
form.setValue('email', email);
}
}, [form]);
return (
<div className={cn('flex justify-center gap-x-12', className)}>
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">

View File

@ -1,138 +0,0 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast';
const ZSupportTicketSchema = z.object({
subject: z.string().min(3, 'Subject is required'),
message: z.string().min(10, 'Message must be at least 10 characters'),
});
type TSupportTicket = z.infer<typeof ZSupportTicketSchema>;
export type SupportTicketFormProps = {
organisationId: string;
teamId?: string | null;
onSuccess?: () => void;
onClose?: () => void;
};
export const SupportTicketForm = ({
organisationId,
teamId,
onSuccess,
onClose,
}: SupportTicketFormProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { mutateAsync: submitSupportTicket, isPending } =
trpc.profile.submitSupportTicket.useMutation();
const form = useForm<TSupportTicket>({
resolver: zodResolver(ZSupportTicketSchema),
defaultValues: {
subject: '',
message: '',
},
});
const isLoading = form.formState.isLoading || isPending;
const onSubmit = async (data: TSupportTicket) => {
const { subject, message } = data;
try {
await submitSupportTicket({
subject,
message,
organisationId,
teamId,
});
toast({
title: t`Support ticket created`,
description: t`Your support request has been submitted. We'll get back to you soon!`,
});
if (onSuccess) {
onSuccess();
}
form.reset();
} catch (err) {
toast({
title: t`Failed to create support ticket`,
description: t`An error occurred. Please try again later.`,
variant: 'destructive',
});
}
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset disabled={isLoading} className="flex flex-col gap-4">
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Subject</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Message</Trans>
</FormLabel>
<FormControl>
<Textarea rows={5} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-2 flex flex-row gap-2">
<Button type="submit" size="sm" loading={isLoading}>
<Trans>Submit</Trans>
</Button>
{onClose && (
<Button variant="outline" size="sm" type="button" onClick={onClose}>
<Trans>Close</Trans>
</Button>
)}
</div>
</fieldset>
</form>
</Form>
</>
);
};

View File

@ -0,0 +1,63 @@
import { Trans } from '@lingui/react/macro';
import { LinkIcon } from 'lucide-react';
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
export type DocumentSigningAttachmentsDialogProps = {
document: DocumentAndSender;
};
export const DocumentSigningAttachmentsDialog = ({
document,
}: DocumentSigningAttachmentsDialogProps) => {
const attachments = document.attachments ?? [];
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<Trans>Attachments</Trans>
</Button>
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Attachments</Trans>
</DialogTitle>
<DialogDescription>
<Trans>View all attachments for this document.</Trans>
</DialogDescription>
</DialogHeader>
<div className="mt-2 flex flex-col gap-2">
{attachments.length === 0 && (
<span className="text-muted-foreground text-sm">
<Trans>No attachments available.</Trans>
</span>
)}
{attachments.map((attachment, idx) => (
<a
key={attachment.id || idx}
href={attachment.url}
target="_blank"
rel="noopener noreferrer"
className="hover:bg-muted/50 flex items-center gap-2 rounded px-2 py-1"
>
<LinkIcon className="h-4 w-4" />
<span className="truncate">{attachment.label}</span>
</a>
))}
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -9,6 +9,7 @@ import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { P, match } from 'ts-pattern';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { AUTO_SIGNABLE_FIELD_TYPES } from '@documenso/lib/constants/autosign';
import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
@ -60,6 +61,12 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
const { email, fullName } = useRequiredDocumentSigningContext();
const { derivedRecipientActionAuth } = useRequiredDocumentSigningAuthContext();
const [open, setOpen] = useState(false);
const form = useForm();
const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation();
const autoSignableFields = fields.filter((field) => {
if (field.inserted) {
return false;
@ -88,14 +95,6 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
(actionAuth) => !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(actionAuth),
);
const [open, setOpen] = useState(() => {
return actionAuthAllowsAutoSign && autoSignableFields.length > AUTO_SIGN_THRESHOLD;
});
const form = useForm();
const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation();
const onSubmit = async () => {
const results = await Promise.allSettled(
autoSignableFields.map(async (field) => {
@ -153,6 +152,12 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
await revalidate();
};
unsafe_useEffectOnce(() => {
if (actionAuthAllowsAutoSign && autoSignableFields.length > AUTO_SIGN_THRESHOLD) {
setOpen(true);
}
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>

View File

@ -16,6 +16,7 @@ import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/uti
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
@ -176,7 +177,15 @@ export const DocumentSigningForm = ({
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
return (
<div className="flex h-full flex-col">
<div
className={cn(
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
{
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': user,
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
},
)}
>
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
<Trans>Click to insert field</Trans>
@ -185,8 +194,21 @@ export const DocumentSigningForm = ({
<div className="custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2">
<div className="flex flex-1 flex-col">
<h3 className="text-foreground text-2xl font-semibold">
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
{recipient.role === RecipientRole.ASSISTANT && <Trans>Assist Document</Trans>}
</h3>
{recipient.role === RecipientRole.VIEWER ? (
<>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>Please mark as viewed to complete</Trans>
</p>
<hr className="border-border mb-8 mt-4" />
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
<div className="flex flex-1 flex-col gap-y-4" />
<div className="flex flex-col gap-4 md:flex-row">
@ -223,6 +245,15 @@ export const DocumentSigningForm = ({
) : recipient.role === RecipientRole.ASSISTANT ? (
<>
<form onSubmit={assistantForm.handleSubmit(onAssistantFormSubmit)}>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
Complete the fields for the following signers. Once reviewed, they will inform
you if any modifications are needed.
</Trans>
</p>
<hr className="border-border my-4" />
<fieldset className="dark:bg-background border-border rounded-2xl border bg-white p-3">
<Controller
name="selectedSignerId"
@ -309,76 +340,88 @@ export const DocumentSigningForm = ({
</>
) : (
<>
<fieldset
disabled={isSubmitting}
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
>
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<div>
<p className="text-muted-foreground mt-2 text-sm">
{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? (
<Trans>Please review the document before approving.</Trans>
) : (
<Trans>Please review the document before signing.</Trans>
)}
</p>
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
<hr className="border-border mb-8 mt-4" />
{hasSignatureField && (
<fieldset
disabled={isSubmitting}
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
>
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
<Label htmlFor="full-name">
<Trans>Full Name</Trans>
</Label>
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
<Input
type="text"
id="full-name"
className="bg-background mt-2"
value={fullName}
onChange={(e) => setFullName(e.target.value.trimStart())}
/>
</div>
)}
{hasSignatureField && (
<div>
<Label htmlFor="Signature">
<Trans>Signature</Trans>
</Label>
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
/>
</div>
)}
</div>
</fieldset>
<div className="mt-6 flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={async () => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>
<DocumentSigningCompleteDialog
isSubmitting={isSubmitting || isAssistantSubmitting}
documentTitle={document.title}
fields={fields}
fieldsValidated={fieldsValidated}
disabled={!isRecipientsTurn}
onSignatureComplete={async (nextSigner) => {
await completeDocument(undefined, nextSigner);
}}
role={recipient.role}
allowDictateNextSigner={
nextRecipient && document.documentMeta?.allowDictateNextSigner
}
defaultNextSigner={
nextRecipient
? { name: nextRecipient.name, email: nextRecipient.email }
: undefined
}
/>
</div>
</fieldset>
<div className="mt-6 flex flex-col gap-4 md:flex-row">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={async () => navigate(-1)}
>
<Trans>Cancel</Trans>
</Button>
<DocumentSigningCompleteDialog
isSubmitting={isSubmitting || isAssistantSubmitting}
documentTitle={document.title}
fields={fields}
fieldsValidated={fieldsValidated}
disabled={!isRecipientsTurn}
onSignatureComplete={async (nextSigner) => {
await completeDocument(undefined, nextSigner);
}}
role={recipient.role}
allowDictateNextSigner={
nextRecipient && document.documentMeta?.allowDictateNextSigner
}
defaultNextSigner={
nextRecipient
? { name: nextRecipient.name, email: nextRecipient.email }
: undefined
}
/>
</div>
</>
)}

View File

@ -3,7 +3,6 @@ import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import type { Field } from '@prisma/client';
import { FieldType, RecipientRole } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { match } from 'ts-pattern';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@ -21,11 +20,11 @@ import type { CompletedField } from '@documenso/lib/types/fields';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { DocumentSigningAttachmentsDialog } from '~/components/general/document-signing/document-signing-attachments-dialog';
import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign';
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field';
@ -64,7 +63,6 @@ export const DocumentSigningPageView = ({
const { documentData, documentMeta } = document;
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
const [isExpanded, setIsExpanded] = useState(false);
let senderName = document.user.name ?? '';
let senderEmail = `(${document.user.email})`;
@ -80,15 +78,15 @@ export const DocumentSigningPageView = ({
return (
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
<div className="mx-auto w-full max-w-screen-xl sm:px-6">
<div className="mx-auto w-full max-w-screen-xl">
<h1
className="block max-w-[20rem] truncate text-2xl font-semibold sm:mt-4 md:max-w-[30rem] md:text-3xl"
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title}
</h1>
<div className="mt-1.5 flex flex-wrap items-center justify-between gap-y-2 sm:mt-2.5 sm:gap-y-0">
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-x-6 gap-y-4">
<div className="max-w-[50ch]">
<span className="text-muted-foreground truncate" title={senderName}>
{senderName} {senderEmail}
@ -135,82 +133,32 @@ export const DocumentSigningPageView = ({
</span>
</div>
<DocumentSigningRejectDialog document={document} token={recipient.token} />
<div className="flex gap-2">
<DocumentSigningAttachmentsDialog document={document} />
<DocumentSigningRejectDialog document={document} token={recipient.token} />
</div>
</div>
<div className="relative mt-4 flex w-full flex-col gap-x-6 gap-y-8 sm:mt-8 md:flex-row lg:gap-x-8 lg:gap-y-0">
<div className="flex-1">
<Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2">
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
</CardContent>
</Card>
</div>
<div
key={isExpanded ? 'expanded' : 'collapsed'}
className="group/document-widget fixed bottom-6 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-4 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
<Card
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
gradient
>
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
{match(recipient.role)
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
.otherwise(() => null)}
</h3>
<CardContent className="p-2">
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
</CardContent>
</Card>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
</Button>
</div>
<div className="hidden group-data-[expanded]/document-widget:block md:block">
<p className="text-muted-foreground mt-2 text-sm">
{match(recipient.role)
.with(RecipientRole.VIEWER, () => (
<Trans>Please mark as viewed to complete.</Trans>
))
.with(RecipientRole.SIGNER, () => (
<Trans>Please review the document before signing.</Trans>
))
.with(RecipientRole.APPROVER, () => (
<Trans>Please review the document before approving.</Trans>
))
.with(RecipientRole.ASSISTANT, () => (
<Trans>Complete the fields for the following signers.</Trans>
))
.otherwise(() => null)}
</p>
<hr className="border-border mb-8 mt-4" />
</div>
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<DocumentSigningForm
document={document}
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
isRecipientsTurn={isRecipientsTurn}
allRecipients={allRecipients}
setSelectedSignerId={setSelectedSignerId}
/>
</div>
</div>
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
<DocumentSigningForm
document={document}
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
isRecipientsTurn={isRecipientsTurn}
allRecipients={allRecipients}
setSelectedSignerId={setSelectedSignerId}
/>
</div>
</div>

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@ -80,16 +80,20 @@ export const DocumentSigningTextField = ({
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField = useMemo(
() =>
(!field.inserted && parsedFieldMeta?.text) ||
(!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly),
[field.inserted, parsedFieldMeta?.text, parsedFieldMeta?.readOnly],
);
const shouldAutoSignField =
(!field.inserted && parsedFieldMeta?.text) ||
(!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly);
const [showCustomTextModal, setShowCustomTextModal] = useState(false);
const [localText, setLocalCustomText] = useState(parsedFieldMeta?.text ?? '');
useEffect(() => {
if (!showCustomTextModal) {
setLocalCustomText(parsedFieldMeta?.text ?? '');
setErrors(initialErrors);
}
}, [showCustomTextModal]);
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = e.target.value;
setLocalCustomText(text);
@ -212,17 +216,30 @@ export const DocumentSigningTextField = ({
}
};
if (shouldAutoSignField) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
actionTarget: field.type,
});
}
useEffect(() => {
if (shouldAutoSignField) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
actionTarget: field.type,
});
}
}, []);
const parsedField = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : undefined;
const labelDisplay = parsedField?.label;
const textDisplay = parsedField?.text;
const labelDisplay =
parsedField?.label && parsedField.label.length < 20
? parsedField.label
: parsedField?.label
? parsedField?.label.substring(0, 20) + '...'
: undefined;
const textDisplay =
parsedField?.text && parsedField.text.length < 20
? parsedField.text
: parsedField?.text
? parsedField?.text.substring(0, 20) + '...'
: undefined;
const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay;
const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0);
@ -312,8 +329,7 @@ export const DocumentSigningTextField = ({
className="flex-1"
onClick={() => {
setShowCustomTextModal(false);
setLocalCustomText(parsedFieldMeta?.text ?? '');
setErrors(initialErrors);
setLocalCustomText('');
}}
>
<Trans>Cancel</Trans>

View File

@ -0,0 +1,192 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id';
import { AttachmentType } from '@documenso/prisma/generated/types';
import { trpc } from '@documenso/trpc/react';
import type { TSetDocumentAttachmentsSchema } from '@documenso/trpc/server/document-router/set-document-attachments.types';
import { ZSetDocumentAttachmentsSchema } from '@documenso/trpc/server/document-router/set-document-attachments.types';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
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';
export type AttachmentFormProps = {
documentId: number;
};
export const AttachmentForm = ({ documentId }: AttachmentFormProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { data: attachmentsData, refetch: refetchAttachments } =
trpc.document.attachments.find.useQuery({
documentId,
});
const { mutateAsync: setDocumentAttachments } = trpc.document.attachments.set.useMutation();
const defaultAttachments = [
{
id: nanoid(12),
label: '',
url: '',
type: AttachmentType.LINK,
},
];
const form = useForm<TSetDocumentAttachmentsSchema>({
resolver: zodResolver(ZSetDocumentAttachmentsSchema),
defaultValues: {
documentId,
attachments: attachmentsData ?? defaultAttachments,
},
});
const {
fields: attachments,
append: appendAttachment,
remove: removeAttachment,
} = useFieldArray({
control: form.control,
name: 'attachments',
});
const onAddAttachment = () => {
appendAttachment({
id: nanoid(12),
label: '',
url: '',
type: AttachmentType.LINK,
});
};
const onRemoveAttachment = (index: number) => {
removeAttachment(index);
};
useEffect(() => {
if (attachmentsData && attachmentsData.length > 0) {
form.setValue('attachments', attachmentsData);
}
}, [attachmentsData]);
const onSubmit = async (data: TSetDocumentAttachmentsSchema) => {
try {
await setDocumentAttachments({
documentId,
attachments: data.attachments,
});
toast({
title: t`Attachment(s) updated`,
description: t`The attachment(s) have been updated successfully`,
});
await refetchAttachments();
} catch (error) {
console.error(error);
toast({
title: t`Something went wrong`,
description: t`We encountered an unknown error while attempting to create the attachments.`,
variant: 'destructive',
duration: 5000,
});
}
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<Trans>Attachments</Trans>
</Button>
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Attachments</Trans>
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
{attachments.map((attachment, index) => (
<div key={attachment.id} className="flex items-end gap-2">
<FormField
control={form.control}
name={`attachments.${index}.label`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>
<Trans>Label</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder={t`Attachment label`} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`attachments.${index}.url`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>
<Trans>URL</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder="https://..." />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onRemoveAttachment(index)}
>
<Trash className="h-4 w-4" />
</Button>
</div>
))}
</fieldset>
<DialogFooter className="mt-4">
<Button type="button" variant="outline" onClick={onAddAttachment}>
<Trans>Add</Trans>
</Button>
<Button type="submit">
<Trans>Save</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -289,7 +289,7 @@ export const DocumentEditForm = ({
message,
distributionMethod,
emailId,
emailReplyTo: emailReplyTo || null,
emailReplyTo,
emailSettings: emailSettings,
},
});

View File

@ -164,7 +164,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
<DropdownMenuItem asChild>
<Link to={`${documentsPath}/${document.id}/logs`}>
<ScrollTextIcon className="mr-2 h-4 w-4" />
<Trans>Audit Logs</Trans>
<Trans>Audit Log</Trans>
</Link>
</DropdownMenuItem>

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@ -26,13 +26,16 @@ export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string })
setSearchParams(params);
},
[searchParams, setSearchParams],
[searchParams],
);
const currentQuery = searchParams?.get('query') ?? '';
if (currentQuery !== debouncedSearchTerm) {
handleSearch(debouncedSearchTerm);
}
useEffect(() => {
const currentQueryParam = searchParams.get('query') || '';
if (debouncedSearchTerm !== currentQueryParam) {
handleSearch(debouncedSearchTerm);
}
}, [debouncedSearchTerm, searchParams]);
return (
<Input

View File

@ -321,19 +321,6 @@ export const OrgMenuSwitcher = () => {
<Trans>Language</Trans>
</DropdownMenuItem>
{currentOrganisation && (
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
<Link
to={{
pathname: `/o/${currentOrganisation.url}/support`,
search: currentTeam ? `?team=${currentTeam.id}` : '',
}}
>
<Trans>Support</Trans>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
onSelect={async () => authClient.signOut()}

View File

@ -0,0 +1,192 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id';
import { AttachmentType } from '@documenso/prisma/generated/types';
import { trpc } from '@documenso/trpc/react';
import type { TSetTemplateAttachmentsSchema } from '@documenso/trpc/server/template-router/set-template-attachments.types';
import { ZSetTemplateAttachmentsSchema } from '@documenso/trpc/server/template-router/set-template-attachments.types';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
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';
export type AttachmentFormProps = {
templateId: number;
};
export const AttachmentForm = ({ templateId }: AttachmentFormProps) => {
const { toast } = useToast();
const { t } = useLingui();
const { data: attachmentsData, refetch: refetchAttachments } =
trpc.template.attachments.find.useQuery({
templateId,
});
const { mutateAsync: setTemplateAttachments } = trpc.template.attachments.set.useMutation();
const defaultAttachments = [
{
id: nanoid(12),
label: '',
url: '',
type: AttachmentType.LINK,
},
];
const form = useForm<TSetTemplateAttachmentsSchema>({
resolver: zodResolver(ZSetTemplateAttachmentsSchema),
defaultValues: {
templateId,
attachments: attachmentsData ?? defaultAttachments,
},
});
const {
fields: attachments,
append: appendAttachment,
remove: removeAttachment,
} = useFieldArray({
control: form.control,
name: 'attachments',
});
const onAddAttachment = () => {
appendAttachment({
id: nanoid(12),
label: '',
url: '',
type: AttachmentType.LINK,
});
};
const onRemoveAttachment = (index: number) => {
removeAttachment(index);
};
useEffect(() => {
if (attachmentsData && attachmentsData.length > 0) {
form.setValue('attachments', attachmentsData);
}
}, [attachmentsData]);
const onSubmit = async (data: TSetTemplateAttachmentsSchema) => {
try {
await setTemplateAttachments({
templateId,
attachments: data.attachments,
});
toast({
title: t`Attachment(s) updated`,
description: t`The attachment(s) have been updated successfully`,
});
await refetchAttachments();
} catch (error) {
console.error(error);
toast({
title: t`Something went wrong`,
description: t`We encountered an unknown error while attempting to create the attachments.`,
variant: 'destructive',
duration: 5000,
});
}
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<Trans>Attachments</Trans>
</Button>
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Attachments</Trans>
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
{attachments.map((attachment, index) => (
<div key={attachment.id} className="flex items-end gap-2">
<FormField
control={form.control}
name={`attachments.${index}.label`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>
<Trans>Label</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder={t`Attachment label`} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`attachments.${index}.url`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel required>
<Trans>URL</Trans>
</FormLabel>
<FormControl>
<Input {...field} placeholder={t`https://...`} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onRemoveAttachment(index)}
>
<Trash className="h-4 w-4" />
</Button>
</div>
))}
</fieldset>
<DialogFooter className="mt-4">
<Button type="button" variant="outline" onClick={onAddAttachment}>
<Trans>Add</Trans>
</Button>
<Button type="submit">
<Trans>Save</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -1,129 +0,0 @@
import { type ReactNode, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import { useNavigate, useParams } from 'react-router';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export interface TemplateDropZoneWrapperProps {
children: ReactNode;
className?: string;
}
export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZoneWrapperProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { folderId } = useParams();
const team = useCurrentTeam();
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
const onFileDrop = async (file: File) => {
try {
setIsLoading(true);
const documentData = await putPdfFile(file);
const { id } = await createTemplate({
title: file.name,
templateDocumentDataId: documentData.id,
folderId: folderId ?? undefined,
});
toast({
title: _(msg`Template uploaded`),
description: _(
msg`Your template has been uploaded successfully. You will be redirected to the template page.`,
),
duration: 5000,
});
await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`);
} catch {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Please try again later.`),
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const onFileDropRejected = () => {
toast({
title: _(msg`Your template failed to upload.`),
description: _(msg`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`),
duration: 5000,
variant: 'destructive',
});
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
//disabled: isUploadDisabled,
multiple: false,
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
onDrop: ([acceptedFile]) => {
if (acceptedFile) {
void onFileDrop(acceptedFile);
}
},
onDropRejected: () => {
void onFileDropRejected();
},
noClick: true,
noDragEventsBubbling: true,
});
return (
<div {...getRootProps()} className={cn('relative min-h-screen', className)}>
<input {...getInputProps()} />
{children}
{isDragActive && (
<div className="bg-muted/60 fixed left-0 top-0 z-[9999] h-full w-full backdrop-blur-[4px]">
<div className="pointer-events-none flex h-full w-full flex-col items-center justify-center">
<h2 className="text-foreground text-2xl font-semibold">
<Trans>Upload Template</Trans>
</h2>
<p className="text-muted-foreground text-md mt-4">
<Trans>Drag and drop your PDF file here</Trans>
</p>
</div>
</div>
)}
{isLoading && (
<div className="bg-muted/30 absolute inset-0 z-50 backdrop-blur-[2px]">
<div className="pointer-events-none flex h-1/2 w-full flex-col items-center justify-center">
<Loader className="text-primary h-12 w-12 animate-spin" />
<p className="text-foreground mt-8 font-medium">
<Trans>Uploading template...</Trans>
</p>
</div>
</div>
)}
</div>
);
};

View File

@ -143,7 +143,6 @@ export const TemplateEditForm = ({
},
meta: {
...data.meta,
emailReplyTo: data.meta.emailReplyTo || null,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),

View File

@ -6,8 +6,6 @@ import { PenIcon, PlusIcon } from 'lucide-react';
import { Link } from 'react-router';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
export type TemplatePageViewRecipientsProps = {
@ -55,18 +53,8 @@ export const TemplatePageViewRecipients = ({
{recipients.map((recipient) => (
<li key={recipient.id} className="flex items-center justify-between px-4 py-2.5 text-sm">
<AvatarWithText
avatarFallback={
isTemplateRecipientEmailPlaceholder(recipient.email)
? extractInitials(recipient.name)
: recipient.email.slice(0, 1).toUpperCase()
}
primaryText={
isTemplateRecipientEmailPlaceholder(recipient.email) ? (
<p className="text-muted-foreground text-sm">{recipient.name}</p>
) : (
<p className="text-muted-foreground text-sm">{recipient.email}</p>
)
}
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
primaryText={<p className="text-muted-foreground text-sm">{recipient.email}</p>}
secondaryText={
<p className="text-muted-foreground/70 text-xs">
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}

View File

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@ -22,34 +22,11 @@ export type VerifyEmailBannerProps = {
const RESEND_CONFIRMATION_EMAIL_TIMEOUT = 20 * ONE_SECOND;
const shouldShowDialog = () => {
try {
const emailVerificationDialogLastShown = localStorage.getItem(
'emailVerificationDialogLastShown',
);
if (emailVerificationDialogLastShown) {
const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
if (Date.now() - lastShownTimestamp < ONE_DAY) {
return false;
}
}
// Update the timestamp when showing the dialog
localStorage.setItem('emailVerificationDialogLastShown', Date.now().toString());
return true;
} catch {
// In case localStorage is not available (SSR, incognito mode, etc.)
return false;
}
};
export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(shouldShowDialog);
const [isOpen, setIsOpen] = useState(false);
const [isPending, setIsPending] = useState(false);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
@ -85,6 +62,27 @@ export const VerifyEmailBanner = ({ email }: VerifyEmailBannerProps) => {
setIsPending(false);
};
useEffect(() => {
// Check localStorage to see if we've recently automatically displayed the dialog
// if it was within the past 24 hours, don't show it again
// otherwise, show it again and update the localStorage timestamp
const emailVerificationDialogLastShown = localStorage.getItem(
'emailVerificationDialogLastShown',
);
if (emailVerificationDialogLastShown) {
const lastShownTimestamp = parseInt(emailVerificationDialogLastShown);
if (Date.now() - lastShownTimestamp < ONE_DAY) {
return;
}
}
setIsOpen(true);
localStorage.setItem('emailVerificationDialogLastShown', Date.now().toString());
}, []);
return (
<>
<div className="bg-yellow-200 dark:bg-yellow-400">

View File

@ -1,18 +1,20 @@
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 type { DateTimeFormatOptions } from 'luxon';
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 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 {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@documenso/ui/primitives/table';
export type AuditLogDataTableProps = {
logs: TDocumentAuditLog[];
@ -23,129 +25,71 @@ const dateFormat: DateTimeFormatOptions = {
hourCycle: 'h12',
};
/**
* Get the color indicator for the audit log type
*/
const getAuditLogIndicatorColor = (type: string) =>
match(type)
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => 'bg-green-500')
.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,
),
() => 'bg-blue-500',
)
.otherwise(() => 'bg-muted');
/**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*/
const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UAParser.IResult) => {
if (!userAgent) {
return msg`N/A`;
}
const browser = userAgentInfo.browser.name;
const version = userAgentInfo.browser.version;
const os = userAgentInfo.os.name;
// If we can parse meaningful browser info, format it nicely
if (browser && os) {
const browserInfo = version ? `${browser} ${version}` : browser;
return msg`${browserInfo} on ${os}`;
}
return msg`${userAgent}`;
};
export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
const { _ } = useLingui();
const parser = new UAParser();
const uppercaseFistLetter = (text: string) => {
return text.charAt(0).toUpperCase() + text.slice(1);
};
return (
<div className="space-y-4">
{logs.map((log, index) => {
parser.setUA(log.userAgent || '');
const formattedAction = formatDocumentAuditLogAction(_, log);
const userAgentInfo = parser.getResult();
<Table overflowHidden>
<TableHeader>
<TableRow>
<TableHead>{_(msg`Time`)}</TableHead>
<TableHead>{_(msg`User`)}</TableHead>
<TableHead>{_(msg`Action`)}</TableHead>
<TableHead>{_(msg`IP Address`)}</TableHead>
<TableHead>{_(msg`Browser`)}</TableHead>
</TableRow>
</TableHeader>
return (
<Card
key={index}
// Add top margin for the first card to ensure it's not cut off from the 2nd page onwards
className={`border shadow-sm ${index > 0 ? 'print:mt-8' : ''}`}
style={{
pageBreakInside: 'avoid',
breakInside: 'avoid',
}}
>
<CardContent className="p-4">
{/* 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))}
/>
<TableBody className="print:text-xs">
{logs.map((log, i) => (
<TableRow className="break-inside-avoid" key={i}>
<TableCell>
{DateTime.fromJSDate(log.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
</TableCell>
<div>
<div className="text-muted-foreground text-sm font-medium uppercase tracking-wide print:text-[8pt]">
{log.type.replace(/_/g, ' ')}
</div>
<div className="text-foreground text-sm font-medium print:text-[8pt]">
{formattedAction.description}
</div>
</div>
</div>
<div className="text-muted-foreground text-sm print:text-[8pt]">
{DateTime.fromJSDate(log.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
</div>
</div>
<hr className="my-4" />
{/* Details Section - Two column layout */}
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
<TableCell>
{log.name || log.email ? (
<div>
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
{_(msg`User`)}
</div>
{log.name && (
<p className="break-all" title={log.name}>
{log.name}
</p>
)}
<div className="text-foreground mt-1 font-mono">{log.email || 'N/A'}</div>
{log.email && (
<p className="text-muted-foreground break-all" title={log.email}>
{log.email}
</p>
)}
</div>
) : (
<p>N/A</p>
)}
</TableCell>
<div className="text-right">
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
{_(msg`IP Address`)}
</div>
<TableCell>
{uppercaseFistLetter(formatDocumentAuditLogAction(_, log).description)}
</TableCell>
<div className="text-foreground mt-1 font-mono">{log.ipAddress || 'N/A'}</div>
</div>
<TableCell>{log.ipAddress}</TableCell>
<div className="col-span-2">
<div className="text-muted-foreground/70 font-medium uppercase tracking-wide">
{_(msg`User Agent`)}
</div>
<div className="text-foreground mt-1">
{_(formatUserAgent(log.userAgent, userAgentInfo))}
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
<TableCell>
{log.userAgent ? parser.setUA(log.userAgent).getBrowser().name : 'N/A'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};

View File

@ -46,7 +46,9 @@ export const TemplatesTableActionDropdown = ({
const isOwner = row.userId === user.id;
const isTeamTemplate = row.teamId === teamId;
const formatPath = `${templateRootPath}/${row.id}/edit`;
const formatPath = row.folderId
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
: `${templateRootPath}/${row.id}/edit`;
return (
<DropdownMenu>

View File

@ -27,7 +27,6 @@ import { AdminOrganisationCreateDialog } from '~/components/dialogs/admin-organi
import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
import { AdminUserResetTwoFactorDialog } from '~/components/dialogs/admin-user-reset-two-factor-dialog';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
@ -220,11 +219,10 @@ const AdminUserPage = ({ user }: { user: User }) => {
/>
</div>
<div className="mt-16 flex flex-col gap-4">
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
<div className="mt-16 flex flex-col items-center gap-4">
{user && <AdminUserDeleteDialog user={user} />}
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
{user && <AdminUserDeleteDialog user={user} />}
</div>
</div>
);

View File

@ -46,7 +46,6 @@ export default function OrganisationSettingsDocumentPage() {
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
signatureTypes,
} = data;
@ -55,8 +54,7 @@ export default function OrganisationSettingsDocumentPage() {
documentLanguage === null ||
documentDateFormat === null ||
includeSenderDetails === null ||
includeSigningCertificate === null ||
includeAuditLog === null
includeSigningCertificate === null
) {
throw new Error('Should not be possible.');
}
@ -70,7 +68,6 @@ export default function OrganisationSettingsDocumentPage() {
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),

View File

@ -171,7 +171,7 @@ export default function OrganisationEmailDomainSettingsPage({ params }: Route.Co
<OrganisationEmailDomainRecordsDialog
records={records}
trigger={
<Button variant="outline">
<Button variant="secondary">
<Trans>View DNS Records</Trans>
</Button>
}

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@ -29,29 +29,22 @@ export default function TeamsSettingsMembersPage() {
/**
* Handle debouncing the search query.
*/
const handleSearchQueryChange = useCallback(
(newQuery: string) => {
const params = new URLSearchParams(searchParams?.toString());
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
if (newQuery.trim()) {
params.set('query', newQuery);
} else {
params.delete('query');
}
params.set('query', debouncedSearchQuery);
if (params.toString() === searchParams?.toString()) {
return;
}
if (debouncedSearchQuery === '') {
params.delete('query');
}
setSearchParams(params);
},
[searchParams, setSearchParams],
);
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
const currentParamQuery = searchParams?.get('query') ?? '';
if (currentParamQuery !== debouncedSearchQuery) {
handleSearchQueryChange(debouncedSearchQuery);
}
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
return (
<div>

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useEffect, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useSearchParams } from 'react-router';
@ -20,28 +20,21 @@ export default function OrganisationSettingsTeamsPage() {
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
/**
* Handle debouncing the search query.
*/
const handleSearchQueryChange = useCallback(
(newQuery: string) => {
const params = new URLSearchParams(searchParams?.toString());
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
if (newQuery.trim()) {
params.set('query', newQuery);
} else {
params.delete('query');
}
params.set('query', debouncedSearchQuery);
setSearchParams(params);
},
[searchParams, setSearchParams],
);
if (debouncedSearchQuery === '') {
params.delete('query');
}
const currentParamQuery = searchParams?.get('query') ?? '';
if (currentParamQuery !== debouncedSearchQuery) {
handleSearchQueryChange(debouncedSearchQuery);
}
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
return (
<div>

View File

@ -1,125 +0,0 @@
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { BookIcon, HelpCircleIcon, Link2Icon } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { Button } from '@documenso/ui/primitives/button';
import { SupportTicketForm } from '~/components/forms/support-ticket-form';
import { appMetaTags } from '~/utils/meta';
export function meta() {
return appMetaTags('Support');
}
export default function SupportPage() {
const [showForm, setShowForm] = useState(false);
const { user } = useSession();
const organisation = useCurrentOrganisation();
const [searchParams] = useSearchParams();
const teamId = searchParams.get('team');
const subscriptionStatus = organisation.subscription?.status;
const handleSuccess = () => {
setShowForm(false);
};
const handleCloseForm = () => {
setShowForm(false);
};
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="mb-8">
<h1 className="flex flex-row items-center gap-2 text-3xl font-bold">
<HelpCircleIcon className="text-muted-foreground h-8 w-8" />
<Trans>Support</Trans>
</h1>
<p className="text-muted-foreground mt-2">
<Trans>Your current plan includes the following support channels:</Trans>
</p>
<div className="mt-6 flex flex-col gap-4">
<div className="rounded-lg border p-4">
<h2 className="flex items-center gap-2 text-lg font-bold">
<BookIcon className="text-muted-foreground h-5 w-5" />
<Link
to="https://docs.documenso.com"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
<Trans>Documentation</Trans>
</Link>
</h2>
<p className="text-muted-foreground mt-1">
<Trans>Read our documentation to get started with Documenso.</Trans>
</p>
</div>
<div className="rounded-lg border p-4">
<h2 className="flex items-center gap-2 text-lg font-bold">
<Link2Icon className="text-muted-foreground h-5 w-5" />
<Link
to="https://documen.so/discord"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
<Trans>Discord</Trans>
</Link>
</h2>
<p className="text-muted-foreground mt-1">
<Trans>
Join our community on{' '}
<Link
to="https://documen.so/discord"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
Discord
</Link>{' '}
for community support and discussion.
</Trans>
</p>
</div>
{organisation && IS_BILLING_ENABLED() && subscriptionStatus && (
<>
<div className="rounded-lg border p-4">
<h2 className="flex items-center gap-2 text-lg font-bold">
<Link2Icon className="text-muted-foreground h-5 w-5" />
<Trans>Contact us</Trans>
</h2>
<p className="text-muted-foreground mt-1">
<Trans>We'll get back to you as soon as possible via email.</Trans>
</p>
<div className="mt-4">
{!showForm ? (
<Button variant="outline" size="sm" onClick={() => setShowForm(true)}>
<Trans>Create a support ticket</Trans>
</Button>
) : (
<SupportTicketForm
organisationId={organisation.id}
teamId={teamId}
onSuccess={handleSuccess}
onClose={handleCloseForm}
/>
)}
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}

View File

@ -1,3 +1,5 @@
import { useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router';
import { OrganisationProvider } from '@documenso/lib/client-only/providers/organisation';
@ -28,8 +30,13 @@ export default function Layout() {
const currentOrganisation = organisations[0];
const team = currentOrganisation?.teams[0] || null;
useEffect(() => {
if (!isPersonalLayoutMode || !team) {
void navigate('/settings/profile');
}
}, []);
if (!isPersonalLayoutMode || !team) {
void navigate('/settings/profile');
return null;
}

View File

@ -10,7 +10,6 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { logDocumentAccess } from '@documenso/lib/utils/logger';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge';
@ -84,12 +83,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
throw redirect(documentRootPath);
}
logDocumentAccess({
request,
documentId,
userId: user.id,
});
return superLoaderJson({
document,
documentRootPath,

View File

@ -9,9 +9,9 @@ import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { logDocumentAccess } from '@documenso/lib/utils/logger';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { AttachmentForm } from '~/components/general/document/document-attachment-form';
import { DocumentEditForm } from '~/components/general/document/document-edit-form';
import { DocumentStatus } from '~/components/general/document/document-status';
import { LegacyFieldWarningPopover } from '~/components/general/legacy-field-warning-popover';
@ -79,12 +79,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
throw redirect(`${documentRootPath}/${documentId}`);
}
logDocumentAccess({
request,
documentId,
userId: user.id,
});
return superLoaderJson({
document: {
...document,
@ -106,7 +100,7 @@ export default function DocumentEditPage() {
<Trans>Documents</Trans>
</Link>
<div className="mt-4 flex w-full items-end justify-between">
<div className="mt-4 flex w-full flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div className="flex-1">
<h1
className="block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
@ -140,11 +134,12 @@ export default function DocumentEditPage() {
</div>
</div>
{document.useLegacyFieldInsertion && (
<div>
<div className={document.useLegacyFieldInsertion ? 'flex items-center gap-2' : undefined}>
{document.useLegacyFieldInsertion && (
<LegacyFieldWarningPopover type="document" documentId={document.id} />
</div>
)}
)}
<AttachmentForm documentId={document.id} />
</div>
</div>
<DocumentEditForm

View File

@ -11,7 +11,6 @@ import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { logDocumentAccess } from '@documenso/lib/utils/logger';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Card } from '@documenso/ui/primitives/card';
@ -60,12 +59,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
teamId: team?.id,
});
logDocumentAccess({
request,
documentId,
userId: user.id,
});
return {
document,
documentRootPath,
@ -177,7 +170,7 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
<ul className="text-muted-foreground list-inside list-disc">
{recipients.map((recipient) => (
<li key={`recipient-${recipient.id}`}>
<span>{formatRecipientText(recipient)}</span>
<span className="-ml-2">{formatRecipientText(recipient)}</span>
</li>
))}
</ul>

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { FolderType, OrganisationType } from '@prisma/client';
@ -12,7 +12,10 @@ import { parseToIntegerArray } from '@documenso/lib/utils/params';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { trpc } from '@documenso/trpc/react';
import { ZFindDocumentsInternalRequestSchema } from '@documenso/trpc/server/document-router/schema';
import {
type TFindDocumentsInternalResponse,
ZFindDocumentsInternalRequestSchema,
} from '@documenso/trpc/server/document-router/schema';
import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
@ -52,6 +55,15 @@ export default function DocumentsPage() {
const [isMovingDocument, setIsMovingDocument] = useState(false);
const [documentToMove, setDocumentToMove] = useState<number | null>(null);
const [stats, setStats] = useState<TFindDocumentsInternalResponse['stats']>({
[ExtendedDocumentStatus.DRAFT]: 0,
[ExtendedDocumentStatus.PENDING]: 0,
[ExtendedDocumentStatus.COMPLETED]: 0,
[ExtendedDocumentStatus.REJECTED]: 0,
[ExtendedDocumentStatus.INBOX]: 0,
[ExtendedDocumentStatus.ALL]: 0,
});
const findDocumentSearchParams = useMemo(
() => ZSearchParamsSchema.safeParse(Object.fromEntries(searchParams.entries())).data || {},
[searchParams],
@ -62,19 +74,6 @@ export default function DocumentsPage() {
folderId,
});
const stats = useMemo(
() =>
data?.stats ?? {
[ExtendedDocumentStatus.DRAFT]: 0,
[ExtendedDocumentStatus.PENDING]: 0,
[ExtendedDocumentStatus.COMPLETED]: 0,
[ExtendedDocumentStatus.REJECTED]: 0,
[ExtendedDocumentStatus.INBOX]: 0,
[ExtendedDocumentStatus.ALL]: 0,
},
[data?.stats],
);
const getTabHref = (value: keyof typeof ExtendedDocumentStatus) => {
const params = new URLSearchParams(searchParams);
@ -105,6 +104,12 @@ export default function DocumentsPage() {
return path;
};
useEffect(() => {
if (data?.stats) {
setStats(data.stats);
}
}, [data?.stats]);
return (
<DocumentDropZoneWrapper>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">

View File

@ -38,7 +38,6 @@ export default function TeamsSettingsPage() {
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
signatureTypes,
} = data;
@ -51,7 +50,6 @@ export default function TeamsSettingsPage() {
documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
...(signatureTypes.length === 0
? {
typedSignatureEnabled: null,

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useEffect, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useLocation, useSearchParams } from 'react-router';
@ -19,33 +19,26 @@ export default function TeamsSettingsMembersPage() {
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
/**
* Handle debouncing the search query.
*/
const handleSearchQueryChange = useCallback(
(newQuery: string) => {
const params = new URLSearchParams(searchParams?.toString());
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
if (newQuery.trim()) {
params.set('query', newQuery);
} else {
params.delete('query');
}
params.set('query', debouncedSearchQuery);
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
if (debouncedSearchQuery === '') {
params.delete('query');
}
setSearchParams(params);
},
[searchParams, setSearchParams],
);
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
const currentParamQuery = searchParams?.get('query') ?? '';
if (currentParamQuery !== debouncedSearchQuery) {
handleSearchQueryChange(debouncedSearchQuery);
}
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
return (
<div>

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@ -130,8 +130,12 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
}
};
useEffect(() => {
setIsPublicProfileVisible(profile.enabled);
}, [profile.enabled]);
return (
<div key={team.id} className="max-w-2xl">
<div className="max-w-2xl">
<SettingsHeader
title={t`Public Profile`}
subtitle={t`You can choose to enable or disable the profile for public view.`}

View File

@ -9,6 +9,7 @@ import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper';
import { LegacyFieldWarningPopover } from '~/components/general/legacy-field-warning-popover';
import { AttachmentForm } from '~/components/general/template/template-attachment-form';
import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
import { TemplateEditForm } from '~/components/general/template/template-edit-form';
import { TemplateType } from '~/components/general/template/template-type';
@ -89,6 +90,7 @@ export default function TemplateEditPage() {
<div className="mt-2 flex items-center gap-2 sm:mt-0 sm:self-end">
<TemplateDirectLinkDialogWrapper template={template} />
<AttachmentForm templateId={template.id} />
{template.useLegacyFieldInsertion && (
<div>

View File

@ -12,7 +12,6 @@ import { FolderGrid } from '~/components/general/folder/folder-grid';
import { TemplatesTable } from '~/components/tables/templates-table';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper';
export function meta() {
return appMetaTags('Templates');
@ -37,54 +36,51 @@ export default function TemplatesPage() {
});
return (
<TemplateDropZoneWrapper>
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
<FolderGrid type={FolderType.TEMPLATE} parentId={folderId ?? null} />
<div className="mt-8">
<div className="flex flex-row items-center">
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
<h1 className="truncate text-2xl font-semibold md:text-3xl">
<Trans>Templates</Trans>
</h1>
</div>
<div className="mt-8">
<div className="flex flex-row items-center">
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-muted-foreground text-xs">
{team.name.slice(0, 1)}
</AvatarFallback>
</Avatar>
{data && data.count === 0 ? (
<div className="text-muted-foreground/60 flex h-96 flex-col items-center justify-center gap-y-4">
<Bird className="h-12 w-12" strokeWidth={1.5} />
<h1 className="truncate text-2xl font-semibold md:text-3xl">
<Trans>Templates</Trans>
</h1>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold">
<Trans>We're all empty</Trans>
</h3>
<div className="mt-8">
{data && data.count === 0 ? (
<div className="text-muted-foreground/60 flex h-96 flex-col items-center justify-center gap-y-4">
<Bird className="h-12 w-12" strokeWidth={1.5} />
<div className="text-center">
<h3 className="text-lg font-semibold">
<Trans>We're all empty</Trans>
</h3>
<p className="mt-2 max-w-[50ch]">
<Trans>
You have not yet created any templates. To create a template please upload
one.
</Trans>
</p>
</div>
<p className="mt-2 max-w-[50ch]">
<Trans>
You have not yet created any templates. To create a template please upload one.
</Trans>
</p>
</div>
) : (
<TemplatesTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
documentRootPath={documentRootPath}
templateRootPath={templateRootPath}
/>
)}
</div>
</div>
) : (
<TemplatesTable
data={data}
isLoading={isLoading}
isLoadingError={isLoadingError}
documentRootPath={documentRootPath}
templateRootPath={templateRootPath}
/>
)}
</div>
</div>
</TemplateDropZoneWrapper>
</div>
);
}

View File

@ -1,5 +0,0 @@
@media print {
html {
font-size: 10pt;
}
}

View File

@ -12,17 +12,10 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
import { getTranslations } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import appStylesheet from '~/app.css?url';
import { BrandingLogo } from '~/components/general/branding-logo';
import { InternalAuditLogTable } from '~/components/tables/internal-audit-log-table';
import type { Route } from './+types/audit-log';
import auditLogStylesheet from './audit-log.print.css?url';
export const links: Route.LinksFunction = () => [
{ rel: 'stylesheet', href: appStylesheet },
{ rel: 'stylesheet', href: auditLogStylesheet },
];
export async function loader({ request }: Route.LoaderArgs) {
const d = new URL(request.url).searchParams.get('d');
@ -83,8 +76,8 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<div className="mb-6 border-b pb-4">
<h1 className="text-xl font-semibold">{_(msg`Audit Log`)}</h1>
<div className="flex items-center">
<h1 className="my-8 text-2xl font-bold">{_(msg`Version History`)}</h1>
</div>
<Card>
@ -164,9 +157,11 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
</CardContent>
</Card>
<div className="mt-8">
<InternalAuditLogTable logs={auditLogs} />
</div>
<Card className="mt-8">
<CardContent className="p-0">
<InternalAuditLogTable logs={auditLogs} />
</CardContent>
</Card>
<div className="my-8 flex-row-reverse">
<div className="flex items-end justify-end gap-x-4">

View File

@ -38,48 +38,33 @@ export default function VerifyEmailPage({ loaderData }: Route.ComponentProps) {
const [state, setState] = useState<keyof typeof EMAIL_VERIFICATION_STATE | null>(null);
const [isLoading, setIsLoading] = useState(false);
const verifyToken = async () => {
setIsLoading(true);
try {
const response = await authClient.emailPassword.verifyEmail({
token,
});
await refreshSession();
setState(response.state);
} catch (err) {
console.error(err);
toast({
title: _(msg`Something went wrong`),
description: _(msg`We were unable to verify your email at this time.`),
});
await navigate('/verify-email');
}
setIsLoading(false);
};
useEffect(() => {
let ignore = false;
const verify = async () => {
try {
setIsLoading(true);
const response = await authClient.emailPassword.verifyEmail({
token,
});
if (ignore) {
return;
}
await refreshSession();
setState(response.state);
} catch (err) {
if (ignore) {
return;
}
console.error(err);
toast({
title: _(msg`Something went wrong`),
description: _(msg`We were unable to verify your email at this time.`),
});
await navigate('/verify-email');
} finally {
if (!ignore) {
setIsLoading(false);
}
}
};
void verify();
return () => {
ignore = true;
};
void verifyToken();
}, []);
if (isLoading || state === null) {

View File

@ -101,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.2-rc.4"
"version": "1.12.2-rc.2"
}

60
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.2-rc.4",
"version": "1.12.2-rc.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.2-rc.4",
"version": "1.12.2-rc.2",
"workspaces": [
"apps/*",
"packages/*"
@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.2-rc.4",
"version": "1.12.2-rc.2",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
@ -3522,15 +3522,6 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"license": "MIT",
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
@ -11835,20 +11826,6 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@team-plain/typescript-sdk": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-5.9.0.tgz",
"integrity": "sha512-AHSXyt1kDt74m9YKZBCRCd6cQjB8QjUNr9cehtR2QHzZ/8yXJPzawPJDqOQ3ms5KvwuYrBx2qT3e6C/zrQ5UtA==",
"license": "MIT",
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"graphql": "^16.6.0",
"lodash.get": "^4.4.2",
"zod": "3.22.4"
}
},
"node_modules/@theguild/remark-mermaid": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz",
@ -13258,6 +13235,7 @@
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@ -13270,23 +13248,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@ -18810,6 +18771,7 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"dev": true,
"funding": [
{
"type": "github",
@ -19885,15 +19847,6 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"license": "MIT"
},
"node_modules/graphql": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
"license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
@ -22376,6 +22329,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
@ -30616,6 +30570,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -36628,7 +36583,6 @@
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3",
"@sindresorhus/slugify": "^2.2.1",
"@team-plain/typescript-sdk": "^5.9.0",
"@vvo/tzdb": "^6.117.0",
"csv-parse": "^5.6.0",
"inngest": "^3.19.13",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.2-rc.4",
"version": "1.12.2-rc.2",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -1178,12 +1178,13 @@ test.describe('Unauthorized Access - Document API V2', () => {
const { user: firstRecipientUser } = await seedUser();
const { user: secondRecipientUser } = await seedUser();
const updatedTemplate = await prisma.template.update({
await prisma.template.update({
where: { id: template.id },
data: {
recipients: {
create: [
{
id: firstRecipientUser.id,
name: firstRecipientUser.name || '',
email: firstRecipientUser.email,
token: nanoid(12),
@ -1192,6 +1193,7 @@ test.describe('Unauthorized Access - Document API V2', () => {
signingStatus: SigningStatus.NOT_SIGNED,
},
{
id: secondRecipientUser.id,
name: secondRecipientUser.name || '',
email: secondRecipientUser.email,
token: nanoid(12),
@ -1202,35 +1204,21 @@ test.describe('Unauthorized Access - Document API V2', () => {
],
},
},
include: {
recipients: true,
},
});
const recipientAId = updatedTemplate.recipients.find(
(recipient) => recipient.email === firstRecipientUser.email,
)?.id;
const recipientBId = updatedTemplate.recipients.find(
(recipient) => recipient.email === secondRecipientUser.email,
)?.id;
if (!recipientAId || !recipientBId) {
throw new Error('Recipient IDs not found');
}
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, {
headers: { Authorization: `Bearer ${tokenB}` },
data: {
templateId: template.id,
recipients: [
{
id: recipientAId,
id: firstRecipientUser.id,
name: firstRecipientUser.name,
email: firstRecipientUser.email,
role: RecipientRole.SIGNER,
},
{
id: recipientBId,
id: secondRecipientUser.id,
name: secondRecipientUser.name,
email: secondRecipientUser.email,
role: RecipientRole.SIGNER,

View File

@ -379,11 +379,10 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
.filter({ hasText: /^Upload Template DocumentDrag & drop your PDF here\.$/ })
.nth(2)
.click();
await page.locator('input[type="file"]').nth(0).waitFor({ state: 'attached' });
await page.locator('input[type="file"]').waitFor({ state: 'attached' });
await page
.locator('input[type="file"]')
.nth(0)
.setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
await page.waitForTimeout(3000);

View File

@ -268,7 +268,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
// Upload document.
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.getByTestId('template-use-dialog-file-input').evaluate((e) => {
page.locator('input[type=file]').evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}
@ -361,7 +361,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
// Upload document.
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.getByTestId('template-use-dialog-file-input').evaluate((e) => {
page.locator('input[type=file]').evaluate((e) => {
if (e instanceof HTMLInputElement) {
e.click();
}

View File

@ -144,11 +144,10 @@ test('[TEMPLATES]: use template', async ({ page }) => {
await page.getByRole('button', { name: 'Use Template' }).click();
// Enter template values.
// Get input with Email label placeholder.
await page.getByLabel('Email').click();
await page.getByLabel('Email').fill(teamMemberUser.email);
await page.getByLabel('Name').click();
await page.getByLabel('Name').fill('name');
await page.getByPlaceholder('recipient.1@documenso.com').click();
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
await page.getByPlaceholder('Recipient 1').click();
await page.getByPlaceholder('Recipient 1').fill('name');
await page.getByRole('button', { name: 'Create as draft' }).click();
await page.waitForURL(/\/t\/.+\/documents/);

View File

@ -1,23 +1,23 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
export const useElementBounds = (elementOrSelector: HTMLElement | string, withScroll = false) => {
const [forceRecalc, setForceRecalc] = useState(0);
const [bounds, setBounds] = useState({
top: 0,
left: 0,
height: 0,
width: 0,
});
const bounds = useMemo(() => {
const calculateBounds = useCallback(() => {
const $el =
typeof elementOrSelector === 'string'
? document.querySelector<HTMLElement>(elementOrSelector)
: elementOrSelector;
if (!$el) {
return {
top: 0,
left: 0,
height: 0,
width: 0,
};
throw new Error('Element not found');
}
if (withScroll) {
@ -32,11 +32,15 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
width,
height,
};
}, [elementOrSelector, withScroll, forceRecalc]);
}, [elementOrSelector, withScroll]);
useEffect(() => {
setBounds(calculateBounds());
}, []);
useEffect(() => {
const onResize = () => {
setForceRecalc((prev) => prev + 1);
setBounds(calculateBounds());
};
window.addEventListener('resize', onResize);
@ -57,7 +61,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
}
const observer = new ResizeObserver(() => {
setForceRecalc((prev) => prev + 1);
setBounds(calculateBounds());
});
observer.observe($el);
@ -65,7 +69,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
return () => {
observer.disconnect();
};
}, [elementOrSelector]);
}, []);
return bounds;
};

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { useMemo } from 'react';
import { RefObject, useEffect, useState } from 'react';
/**
* Calculate the width and height of a text element.
@ -64,7 +64,13 @@ export function useElementScaleSize(
fontSize: number,
fontFamily: string,
) {
return useMemo(() => {
return calculateTextScaleSize(container, text, `${fontSize}px`, fontFamily);
}, [container, text, fontSize, fontFamily]);
const [scalingFactor, setScalingFactor] = useState(1);
useEffect(() => {
const scaleSize = calculateTextScaleSize(container, text, `${fontSize}px`, fontFamily);
setScalingFactor(scaleSize);
}, [text, container, fontFamily, fontSize]);
return scalingFactor;
}

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { Field } from '@prisma/client';
@ -6,20 +6,20 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
export const useFieldPageCoords = (field: Field) => {
const [forceRecalc, setForceRecalc] = useState(0);
const [coords, setCoords] = useState({
x: 0,
y: 0,
height: 0,
width: 0,
});
const coords = useMemo(() => {
const calculateCoords = useCallback(() => {
const $page = document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
);
if (!$page) {
return {
x: 0,
y: 0,
height: 0,
width: 0,
};
return;
}
const { top, left, height, width } = getBoundingClientRect($page);
@ -31,17 +31,21 @@ export const useFieldPageCoords = (field: Field) => {
const fieldHeight = (Number(field.height) / 100) * height;
const fieldWidth = (Number(field.width) / 100) * width;
return {
setCoords({
x: fieldX,
y: fieldY,
height: fieldHeight,
width: fieldWidth,
};
}, [field.height, field.page, field.positionX, field.positionY, field.width, forceRecalc]);
});
}, [field.height, field.page, field.positionX, field.positionY, field.width]);
useEffect(() => {
calculateCoords();
}, [calculateCoords]);
useEffect(() => {
const onResize = () => {
setForceRecalc((prev) => prev + 1);
calculateCoords();
};
window.addEventListener('resize', onResize);
@ -49,7 +53,7 @@ export const useFieldPageCoords = (field: Field) => {
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
}, [calculateCoords]);
useEffect(() => {
const $page = document.querySelector<HTMLElement>(
@ -61,7 +65,7 @@ export const useFieldPageCoords = (field: Field) => {
}
const observer = new ResizeObserver(() => {
setForceRecalc((prev) => prev + 1);
calculateCoords();
});
observer.observe($page);
@ -69,7 +73,7 @@ export const useFieldPageCoords = (field: Field) => {
return () => {
observer.disconnect();
};
}, [field.page]);
}, [calculateCoords, field.page]);
return coords;
};

View File

@ -1,17 +1,11 @@
import { useSyncExternalStore } from 'react';
const subscribe = () => {
return () => {};
};
const getSnapshot = () => {
return true;
};
const getServerSnapshot = () => {
return false;
};
import { useEffect, useState } from 'react';
export const useIsMounted = () => {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted;
};

View File

@ -13,4 +13,4 @@ export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED
export const API_V2_BETA_URL = '/api/v2-beta';
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
export const SUPPORT_EMAIL = 'support@documenso.com';

View File

@ -9,7 +9,6 @@ export const VALID_DATE_FORMAT_VALUES = [
'yyyy-MM-dd',
'dd/MM/yyyy hh:mm a',
'MM/dd/yyyy hh:mm a',
'dd.MM.yyyy HH:mm',
'yyyy-MM-dd HH:mm',
'yy-MM-dd hh:mm a',
'yyyy-MM-dd HH:mm:ss',
@ -41,11 +40,6 @@ export const DATE_FORMATS = [
label: 'MM/DD/YYYY',
value: 'MM/dd/yyyy hh:mm a',
},
{
key: 'DDMMYYYYHHMM',
label: 'DD.MM.YYYY HH:mm',
value: 'dd.MM.yyyy HH:mm',
},
{
key: 'YYYYMMDDHHmm',
label: 'YYYY-MM-DD HH:mm',

View File

@ -49,24 +49,15 @@ type DocumentSignatureTypeData = {
export const DOCUMENT_SIGNATURE_TYPES = {
[DocumentSignatureType.DRAW]: {
label: msg({
message: `Draw`,
context: `Draw signatute type`,
}),
label: msg`Draw`,
value: DocumentSignatureType.DRAW,
},
[DocumentSignatureType.TYPE]: {
label: msg({
message: `Type`,
context: `Type signatute type`,
}),
label: msg`Type`,
value: DocumentSignatureType.TYPE,
},
[DocumentSignatureType.UPLOAD]: {
label: msg({
message: `Upload`,
context: `Upload signatute type`,
}),
label: msg`Upload`,
value: DocumentSignatureType.UPLOAD,
},
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;

View File

@ -4,114 +4,39 @@ import { RecipientRole } from '@prisma/client';
export const RECIPIENT_ROLES_DESCRIPTION = {
[RecipientRole.APPROVER]: {
actionVerb: msg({
message: `Approve`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Approved`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Approving`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Approver`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Approvers`,
context: `Recipient role plural name`,
}),
actionVerb: msg`Approve`,
actioned: msg`Approved`,
progressiveVerb: msg`Approving`,
roleName: msg`Approver`,
roleNamePlural: msg`Approvers`,
},
[RecipientRole.CC]: {
actionVerb: msg({
message: `CC`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `CC'd`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `CC`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Cc`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Ccers`,
context: `Recipient role plural name`,
}),
actionVerb: msg`CC`,
actioned: msg`CC'd`,
progressiveVerb: msg`CC`,
roleName: msg`Cc`,
roleNamePlural: msg`Ccers`,
},
[RecipientRole.SIGNER]: {
actionVerb: msg({
message: `Sign`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Signed`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Signing`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Signer`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Signers`,
context: `Recipient role plural name`,
}),
actionVerb: msg`Sign`,
actioned: msg`Signed`,
progressiveVerb: msg`Signing`,
roleName: msg`Signer`,
roleNamePlural: msg`Signers`,
},
[RecipientRole.VIEWER]: {
actionVerb: msg({
message: `View`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Viewed`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Viewing`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Viewer`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Viewers`,
context: `Recipient role plural name`,
}),
actionVerb: msg`View`,
actioned: msg`Viewed`,
progressiveVerb: msg`Viewing`,
roleName: msg`Viewer`,
roleNamePlural: msg`Viewers`,
},
[RecipientRole.ASSISTANT]: {
actionVerb: msg({
message: `Assist`,
context: `Recipient role action verb`,
}),
actioned: msg({
message: `Assisted`,
context: `Recipient role actioned`,
}),
progressiveVerb: msg({
message: `Assisting`,
context: `Recipient role progressive verb`,
}),
roleName: msg({
message: `Assistant`,
context: `Recipient role name`,
}),
roleNamePlural: msg({
message: `Assistants`,
context: `Recipient role plural name`,
}),
actionVerb: msg`Assist`,
actioned: msg`Assisted`,
progressiveVerb: msg`Assisting`,
roleName: msg`Assistant`,
roleNamePlural: msg`Assistants`,
},
} satisfies Record<keyof typeof RecipientRole, unknown>;

View File

@ -3,10 +3,6 @@ import { msg } from '@lingui/core/macro';
export const TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i;
export const TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX = /Recipient \d+/i;
export const isTemplateRecipientEmailPlaceholder = (email: string) => {
return TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email);
};
export const DIRECT_TEMPLATE_DOCUMENTATION = [
{
title: msg`Enable Direct Link Signing`,

View File

@ -48,7 +48,7 @@ export const run = async ({
type: 'team',
teamId: document.teamId,
},
meta: document.documentMeta,
meta: document.documentMeta || null,
});
const { documentMeta, user: documentOwner } = document;

View File

@ -76,7 +76,7 @@ export const run = async ({
type: 'team',
teamId: document.teamId,
},
meta: document.documentMeta,
meta: document.documentMeta || null,
});
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';

View File

@ -68,7 +68,7 @@ export const run = async ({
type: 'team',
teamId: document.teamId,
},
meta: document.documentMeta,
meta: document.documentMeta || null,
});
const i18n = await getI18nInstance(emailLanguage);

View File

@ -86,7 +86,7 @@ export const run = async ({
type: 'team',
teamId: document.teamId,
},
meta: document.documentMeta,
meta: document.documentMeta || null,
});
const customEmail = document?.documentMeta;

View File

@ -9,7 +9,6 @@ import { signPdf } from '@documenso/signing';
import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
@ -146,24 +145,7 @@ export const run = async ({
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch((e) => {
console.log('Failed to get certificate PDF');
console.error(e);
return null;
})
: null;
const auditLogData = settings.includeAuditLog
? await getAuditLogsPdf({
documentId,
language: document.documentMeta?.language,
}).catch((e) => {
console.log('Failed to get audit logs PDF');
console.error(e);
return null;
})
}).catch(() => null)
: null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
@ -192,16 +174,6 @@ export const run = async ({
});
}
if (auditLogData) {
const auditLogDoc = await PDFDocument.load(auditLogData);
const auditLogPages = await pdfDoc.copyPages(auditLogDoc, auditLogDoc.getPageIndices());
auditLogPages.forEach((page) => {
pdfDoc.addPage(page);
});
}
for (const field of fields) {
if (field.inserted) {
document.useLegacyFieldInsertion

View File

@ -33,7 +33,6 @@
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3",
"@sindresorhus/slugify": "^2.2.1",
"@team-plain/typescript-sdk": "^5.9.0",
"@vvo/tzdb": "^6.117.0",
"csv-parse": "^5.6.0",
"inngest": "^3.19.13",

View File

@ -1,7 +0,0 @@
import { PlainClient } from '@team-plain/typescript-sdk';
import { env } from '@documenso/lib/utils/env';
export const plainClient = new PlainClient({
apiKey: env('NEXT_PRIVATE_PLAIN_API_KEY') ?? '',
});

View File

@ -212,7 +212,7 @@ export const createDocumentV2 = async ({
}),
);
// Todo: Is it necessary to create a full audit logs with all fields and recipients audit logs?
// Todo: Is it necessary to create a full audit log with all fields and recipients audit logs?
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({

Some files were not shown because too many files have changed in this diff Show More