Compare commits

...

8 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan db4d33d039 chore: cleanup useEffect 2025-08-21 21:58:14 +00:00
Catalin Pit 231ef9c27e chore: add support option (#1853) 2025-08-19 20:59:03 +10:00
Catalin Pit 6f35342a83 feat: reset user 2fa from admin panel (#1943) 2025-08-19 13:09:05 +10:00
David Nguyen a51110d276 fix: prevent document unsigning on edit (#1963) 2025-08-18 13:48:51 +10:00
David Nguyen 7f81231467 fix: template e2e tests (#1969) 2025-08-18 12:42:36 +10:00
Lucas Smith 439262fd02 v1.12.2-rc.4 2025-08-16 19:16:29 +10:00
Lucas Smith 93a184355b chore: add translations (#1955) 2025-08-16 19:10:21 +10:00
David Nguyen 1dea0b8fab add dummy teamid (#1968) 2025-08-16 19:09:21 +10:00
84 changed files with 2884 additions and 1715 deletions
+2
View File
@@ -136,3 +136,5 @@ 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=
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -96,16 +96,17 @@ export const AdminOrganisationCreateDialog = ({
}
};
useEffect(() => {
form.reset();
}, [open, form]);
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -0,0 +1,159 @@
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>
);
};
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -73,20 +73,23 @@ 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={(value) => !isPending && onOpenChange(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -83,15 +83,6 @@ 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({
@@ -145,12 +136,22 @@ 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={onOpenChange}>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -80,14 +80,15 @@ export const FolderCreateDialog = ({ type, trigger, ...props }: FolderCreateDial
}
};
useEffect(() => {
if (!isCreateFolderOpen) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [isCreateFolderOpen, form]);
setIsCreateFolderOpen(value);
};
return (
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{trigger ?? (
<Button
@@ -1,5 +1,3 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@@ -92,14 +90,15 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
}
};
useEffect(() => {
if (!isOpen) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [isOpen]);
onOpenChange(value);
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -97,12 +97,13 @@ export const FolderMoveDialog = ({
}
};
useEffect(() => {
if (!isOpen) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
setSearchTerm('');
}
}, [isOpen, form]);
onOpenChange(value);
};
// Filter out the current folder, only show folders of the same type, and filter by search term
const filteredFolders = foldersData?.filter(
@@ -113,7 +114,7 @@ export const FolderMoveDialog = ({
);
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -1,5 +1,3 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@@ -71,15 +69,6 @@ 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;
@@ -110,8 +99,18 @@ 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={onOpenChange}>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -76,7 +76,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
const [selectedPriceId, setSelectedPriceId] = useState<string>('');
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(actionSearchParam === 'add-organisation');
const form = useForm({
resolver: zodResolver(ZCreateOrganisationFormSchema),
@@ -91,6 +91,19 @@ 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({
@@ -126,17 +139,6 @@ 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 ||
@@ -145,11 +147,7 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -314,13 +312,16 @@ const BillingPlanForm = ({
};
},
);
}, [plans]);
}, [plans, t]);
useEffect(() => {
if (value === '' && !canCreateFreeOrganisation) {
onChange(dynamicPlans[0][billingPeriod]?.id ?? '');
if (value === '' && !canCreateFreeOrganisation && dynamicPlans.length > 0) {
const defaultValue = dynamicPlans[0][billingPeriod]?.id ?? '';
if (defaultValue) {
onChange(defaultValue);
}
}
}, [value]);
}, [canCreateFreeOrganisation, dynamicPlans, billingPeriod, onChange, value]);
const onBillingPeriodChange = (billingPeriod: 'monthlyPrice' | 'yearlyPrice') => {
const plan = dynamicPlans.find(
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -93,14 +93,19 @@ export const OrganisationDeleteDialog = ({ trigger }: OrganisationDeleteDialogPr
}
};
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (form.formState.isSubmitting) {
return;
}
if (!value) {
form.reset();
}
}, [open, form]);
setOpen(value);
};
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="destructive">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -73,13 +73,6 @@ 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({
@@ -114,8 +107,17 @@ export const OrganisationEmailCreateDialog = ({
}
};
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
if (!isPending) {
setOpen(value);
}
};
return (
<Dialog {...props} open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -36,6 +36,12 @@ 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: () => {
@@ -58,7 +64,7 @@ export const OrganisationEmailDeleteDialog = ({
});
return (
<Dialog open={open} onOpenChange={(value) => !isDeleting && setOpen(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -75,14 +75,6 @@ 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({
@@ -118,12 +110,18 @@ 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={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -87,23 +87,20 @@ export const OrganisationEmailUpdateDialog = ({
}
};
useEffect(() => {
if (!open) {
return;
const handleOpenChange = (value: boolean) => {
if (value) {
form.reset({
emailName: organisationEmail.emailName,
// replyTo: organisationEmail.replyTo ?? undefined,
});
}
form.reset({
emailName: organisationEmail.emailName,
// replyTo: organisationEmail.replyTo ?? undefined,
});
}, [open, form]);
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger}
</DialogTrigger>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
@@ -117,16 +117,17 @@ export const OrganisationGroupCreateDialog = ({
}
};
useEffect(() => {
form.reset();
}, [open, form]);
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -193,13 +193,6 @@ 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;
@@ -267,12 +260,18 @@ 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={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -106,32 +106,27 @@ export const OrganisationMemberUpdateDialog = ({
}
};
useEffect(() => {
if (!open) {
return;
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;
}
}
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',
});
if (!form.formState.isSubmitting) {
setOpen(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, currentUserOrganisationRole, organisationMemberRole, form, toast]);
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -120,24 +120,21 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
return passkeyName;
};
useEffect(() => {
if (!open) {
const handleDialogOpenChange = (value: boolean) => {
if (!value) {
const defaultPasskeyName = extractDefaultPasskeyName();
form.reset({
passkeyName: defaultPasskeyName,
});
setFormError(null);
}
}, [open, form]);
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleDialogOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button variant="secondary" loading={isPending}>
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { 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 [open, setOpen] = useState(false);
const actionSearchParam = searchParams?.get('action');
const shouldOpenDialog = actionSearchParam === 'add-team';
const [open, setOpen] = useState(shouldOpenDialog);
const { data: fullOrganisation } = trpc.organisation.get.useQuery({
organisationReference: organisation.id,
});
const actionSearchParam = searchParams?.get('action');
const form = useForm({
resolver: zodResolver(ZCreateTeamFormSchema),
defaultValues: {
@@ -85,6 +85,18 @@ 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({
@@ -150,23 +162,8 @@ 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={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -114,14 +114,17 @@ export const TeamDeleteDialog = ({
}
};
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [open, form]);
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="destructive">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -103,18 +103,17 @@ export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDi
}
};
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [open, form]);
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button variant="outline" loading={isPending} className="bg-background">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -92,18 +92,17 @@ export const TeamEmailUpdateDialog = ({
}
};
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [open, form]);
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="outline" className="bg-background">
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { 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,
});
setOpen(false);
handleClose();
} catch {
toast({
title: t`An unknown error occurred`,
@@ -117,17 +117,23 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
}
};
useEffect(() => {
if (!open) {
form.reset();
setStep('SELECT');
const handleClose = () => {
setOpen(false);
form.reset();
setStep('SELECT');
};
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
handleClose();
}
}, [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.
>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -106,22 +106,17 @@ export const TeamGroupUpdateDialog = ({
}
};
useEffect(() => {
if (!open) {
return;
const handleOpenChange = (value: boolean) => {
if (!form.formState.isSubmitting) {
if (value) {
form.reset();
}
setOpen(value);
}
form.reset();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, team.currentTeamRole, teamGroupRole, form, toast]);
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -119,20 +119,17 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
}
};
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
setStep('SELECT');
}
}, [open, form]);
// 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
};
return (
<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.
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
<Button variant="secondary" onClick={() => setOpen(true)}>
<Trans>Add members</Trans>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -106,30 +106,25 @@ export const TeamMemberUpdateDialog = ({
}
};
useEffect(() => {
if (!open) {
return;
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;
}
}
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',
});
if (!form.formState.isSubmitting) {
setOpen(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, currentUserTeamRole, memberTeamRole, form, toast]);
};
return (
<Dialog
{...props}
open={open}
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
>
<Dialog {...props} open={open} onOpenChange={handleOpenChange}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
{trigger ?? (
<Button variant="secondary">
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -186,16 +186,20 @@ export const TemplateDirectLinkDialog = ({
const isLoading =
isCreatingTemplateDirectLink || isTogglingTemplateAccess || isDeletingTemplateDirectLink;
useEffect(() => {
resetCreateTemplateDirectLink();
setCurrentStep(token ? 'MANAGE' : 'ONBOARD');
setSelectedRecipientId(null);
const handleOpenChange = (value: boolean) => {
if (!isLoading) {
if (value) {
resetCreateTemplateDirectLink();
setCurrentStep(token ? 'MANAGE' : 'ONBOARD');
setSelectedRecipientId(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
onOpenChange(value);
}
};
return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<fieldset disabled={isLoading} className="relative">
<AnimateGenericFadeInOut motionKey={currentStep}>
{match({ token, currentStep })
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -85,15 +85,6 @@ 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({
@@ -137,12 +128,22 @@ 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={onOpenChange}>
<Dialog {...props} open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -203,14 +203,17 @@ export function TemplateUseDialog({
name: 'recipients',
});
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (form.formState.isSubmitting) return;
if (!value) {
form.reset();
}
}, [open, form]);
setOpen(value);
};
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{trigger || (
<Button variant="outline" className="bg-background">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { 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
}
};
useEffect(() => {
if (!isOpen) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [isOpen, form]);
if (!form.formState.isSubmitting) {
setIsOpen(value);
}
};
return (
<Dialog
open={isOpen}
onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}
>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild={true}>
{children ?? (
<Button className="mr-4" variant="destructive">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -88,14 +88,17 @@ export const WebhookDeleteDialog = ({ webhook, children }: WebhookDeleteDialogPr
}
};
useEffect(() => {
if (!open) {
const handleOpenChange = (value: boolean) => {
if (!value) {
form.reset();
}
}, [open, form]);
if (!form.formState.isSubmitting) {
setOpen(value);
}
};
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{children ?? (
<Button className="mr-4" variant="destructive">
@@ -1,12 +1,10 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { Trans } from '@lingui/react/macro';
export const EmbedDocumentWaitingForTurn = () => {
const [hasPostedMessage, setHasPostedMessage] = useState(false);
useEffect(() => {
if (window.parent && !hasPostedMessage) {
if (window.parent) {
window.parent.postMessage(
{
action: 'document-waiting-for-turn',
@@ -15,13 +13,7 @@ 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">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
@@ -136,18 +136,19 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA
setIsOpen(true);
};
useEffect(() => {
enable2FAForm.reset();
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
setRecoveryCodes(null);
if (!open) {
enable2FAForm.reset();
if (recoveryCodes && recoveryCodes.length > 0) {
setRecoveryCodes(null);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild={true}>
<Button
className="flex-shrink-0"
+11 -14
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
@@ -116,9 +116,18 @@ 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: initialEmail ?? '',
email: emailFromHash ?? initialEmail ?? '',
password: '',
totpCode: '',
backupCode: '',
@@ -287,18 +296,6 @@ 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
+11 -14
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useMemo } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import type { MessageDescriptor } from '@lingui/core';
@@ -84,10 +84,19 @@ 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: initialEmail ?? '',
email: emailFromHash ?? initialEmail ?? '',
password: '',
signature: '',
},
@@ -162,18 +171,6 @@ 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">
@@ -0,0 +1,138 @@
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>
</>
);
};
@@ -9,7 +9,6 @@ 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';
@@ -61,12 +60,6 @@ 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;
@@ -95,6 +88,14 @@ 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) => {
@@ -152,12 +153,6 @@ 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>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -80,20 +80,16 @@ export const DocumentSigningTextField = ({
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField =
(!field.inserted && parsedFieldMeta?.text) ||
(!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly);
const shouldAutoSignField = useMemo(
() =>
(!field.inserted && parsedFieldMeta?.text) ||
(!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly),
[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);
@@ -216,14 +212,12 @@ export const DocumentSigningTextField = ({
}
};
useEffect(() => {
if (shouldAutoSignField) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
actionTarget: field.type,
});
}
}, []);
if (shouldAutoSignField) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
actionTarget: field.type,
});
}
const parsedField = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : undefined;
@@ -318,7 +312,8 @@ export const DocumentSigningTextField = ({
className="flex-1"
onClick={() => {
setShowCustomTextModal(false);
setLocalCustomText('');
setLocalCustomText(parsedFieldMeta?.text ?? '');
setErrors(initialErrors);
}}
>
<Trans>Cancel</Trans>
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -26,16 +26,13 @@ export const DocumentSearch = ({ initialValue = '' }: { initialValue?: string })
setSearchParams(params);
},
[searchParams],
[searchParams, setSearchParams],
);
useEffect(() => {
const currentQueryParam = searchParams.get('query') || '';
if (debouncedSearchTerm !== currentQueryParam) {
handleSearch(debouncedSearchTerm);
}
}, [debouncedSearchTerm, searchParams]);
const currentQuery = searchParams?.get('query') ?? '';
if (currentQuery !== debouncedSearchTerm) {
handleSearch(debouncedSearchTerm);
}
return (
<Input
@@ -321,6 +321,19 @@ 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()}
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -22,11 +22,34 @@ 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(false);
const [isOpen, setIsOpen] = useState(shouldShowDialog);
const [isPending, setIsPending] = useState(false);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
@@ -62,27 +85,6 @@ 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">
@@ -27,6 +27,7 @@ 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';
@@ -219,10 +220,11 @@ const AdminUserPage = ({ user }: { user: User }) => {
/>
</div>
<div className="mt-16 flex flex-col items-center gap-4">
{user && <AdminUserDeleteDialog user={user} />}
<div className="mt-16 flex flex-col gap-4">
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
{user && <AdminUserDeleteDialog user={user} />}
</div>
</div>
);
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
@@ -29,22 +29,29 @@ export default function TeamsSettingsMembersPage() {
/**
* Handle debouncing the search query.
*/
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
const handleSearchQueryChange = useCallback(
(newQuery: string) => {
const params = new URLSearchParams(searchParams?.toString());
params.set('query', debouncedSearchQuery);
if (newQuery.trim()) {
params.set('query', newQuery);
} else {
params.delete('query');
}
if (debouncedSearchQuery === '') {
params.delete('query');
}
if (params.toString() === searchParams?.toString()) {
return;
}
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
setSearchParams(params);
},
[searchParams, setSearchParams],
);
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
const currentParamQuery = searchParams?.get('query') ?? '';
if (currentParamQuery !== debouncedSearchQuery) {
handleSearchQueryChange(debouncedSearchQuery);
}
return (
<div>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useSearchParams } from 'react-router';
@@ -20,21 +20,28 @@ export default function OrganisationSettingsTeamsPage() {
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
/**
* Handle debouncing the search query.
*/
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
const handleSearchQueryChange = useCallback(
(newQuery: string) => {
const params = new URLSearchParams(searchParams?.toString());
params.set('query', debouncedSearchQuery);
if (newQuery.trim()) {
params.set('query', newQuery);
} else {
params.delete('query');
}
if (debouncedSearchQuery === '') {
params.delete('query');
}
setSearchParams(params);
},
[searchParams, setSearchParams],
);
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
const currentParamQuery = searchParams?.get('query') ?? '';
if (currentParamQuery !== debouncedSearchQuery) {
handleSearchQueryChange(debouncedSearchQuery);
}
return (
<div>
@@ -0,0 +1,125 @@
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>
);
}
@@ -1,5 +1,3 @@
import { useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router';
import { OrganisationProvider } from '@documenso/lib/client-only/providers/organisation';
@@ -30,13 +28,8 @@ 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;
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { FolderType, OrganisationType } from '@prisma/client';
@@ -12,10 +12,7 @@ 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 {
type TFindDocumentsInternalResponse,
ZFindDocumentsInternalRequestSchema,
} from '@documenso/trpc/server/document-router/schema';
import { 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';
@@ -55,15 +52,6 @@ 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],
@@ -74,6 +62,19 @@ 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);
@@ -104,12 +105,6 @@ 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">
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useLocation, useSearchParams } from 'react-router';
@@ -19,26 +19,33 @@ export default function TeamsSettingsMembersPage() {
const [searchQuery, setSearchQuery] = useState(() => searchParams?.get('query') ?? '');
const debouncedSearchQuery = useDebouncedValue(searchQuery, 500);
/**
* Handle debouncing the search query.
*/
useEffect(() => {
const params = new URLSearchParams(searchParams?.toString());
const handleSearchQueryChange = useCallback(
(newQuery: string) => {
const params = new URLSearchParams(searchParams?.toString());
params.set('query', debouncedSearchQuery);
if (newQuery.trim()) {
params.set('query', newQuery);
} else {
params.delete('query');
}
if (debouncedSearchQuery === '') {
params.delete('query');
}
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
// If nothing to change then do nothing.
if (params.toString() === searchParams?.toString()) {
return;
}
setSearchParams(params);
},
[searchParams, setSearchParams],
);
setSearchParams(params);
}, [debouncedSearchQuery, pathname, searchParams]);
const currentParamQuery = searchParams?.get('query') ?? '';
if (currentParamQuery !== debouncedSearchQuery) {
handleSearchQueryChange(debouncedSearchQuery);
}
return (
<div>
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@@ -130,12 +130,8 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
}
};
useEffect(() => {
setIsPublicProfileVisible(profile.enabled);
}, [profile.enabled]);
return (
<div className="max-w-2xl">
<div key={team.id} className="max-w-2xl">
<SettingsHeader
title={t`Public Profile`}
subtitle={t`You can choose to enable or disable the profile for public view.`}
@@ -38,33 +38,48 @@ 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(() => {
void verifyToken();
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;
};
}, []);
if (isLoading || state === null) {
+1 -1
View File
@@ -101,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.2-rc.3"
"version": "1.12.2-rc.4"
}
+53 -7
View File
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.2-rc.3",
"version": "1.12.2-rc.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.2-rc.3",
"version": "1.12.2-rc.4",
"workspaces": [
"apps/*",
"packages/*"
@@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.2-rc.3",
"version": "1.12.2-rc.4",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
@@ -3522,6 +3522,15 @@
"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",
@@ -11826,6 +11835,20 @@
"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",
@@ -13235,7 +13258,6 @@
"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",
@@ -13248,6 +13270,23 @@
"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",
@@ -18771,7 +18810,6 @@
"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",
@@ -19847,6 +19885,15 @@
"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",
@@ -22329,7 +22376,6 @@
"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": {
@@ -30570,7 +30616,6 @@
"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"
@@ -36583,6 +36628,7 @@
"@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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.2-rc.3",
"version": "1.12.2-rc.4",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",
@@ -144,10 +144,11 @@ test('[TEMPLATES]: use template', async ({ page }) => {
await page.getByRole('button', { name: 'Use Template' }).click();
// Enter template values.
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');
// 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.getByRole('button', { name: 'Create as draft' }).click();
await page.waitForURL(/\/t\/.+\/documents/);
@@ -1,23 +1,23 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
export const useElementBounds = (elementOrSelector: HTMLElement | string, withScroll = false) => {
const [bounds, setBounds] = useState({
top: 0,
left: 0,
height: 0,
width: 0,
});
const [forceRecalc, setForceRecalc] = useState(0);
const calculateBounds = useCallback(() => {
const bounds = useMemo(() => {
const $el =
typeof elementOrSelector === 'string'
? document.querySelector<HTMLElement>(elementOrSelector)
: elementOrSelector;
if (!$el) {
throw new Error('Element not found');
return {
top: 0,
left: 0,
height: 0,
width: 0,
};
}
if (withScroll) {
@@ -32,15 +32,11 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
width,
height,
};
}, [elementOrSelector, withScroll]);
useEffect(() => {
setBounds(calculateBounds());
}, []);
}, [elementOrSelector, withScroll, forceRecalc]);
useEffect(() => {
const onResize = () => {
setBounds(calculateBounds());
setForceRecalc((prev) => prev + 1);
};
window.addEventListener('resize', onResize);
@@ -61,7 +57,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
}
const observer = new ResizeObserver(() => {
setBounds(calculateBounds());
setForceRecalc((prev) => prev + 1);
});
observer.observe($el);
@@ -69,7 +65,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
return () => {
observer.disconnect();
};
}, []);
}, [elementOrSelector]);
return bounds;
};
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { RefObject, useEffect, useState } from 'react';
import { useMemo } from 'react';
/**
* Calculate the width and height of a text element.
@@ -64,13 +64,7 @@ export function useElementScaleSize(
fontSize: number,
fontFamily: string,
) {
const [scalingFactor, setScalingFactor] = useState(1);
useEffect(() => {
const scaleSize = calculateTextScaleSize(container, text, `${fontSize}px`, fontFamily);
setScalingFactor(scaleSize);
}, [text, container, fontFamily, fontSize]);
return scalingFactor;
return useMemo(() => {
return calculateTextScaleSize(container, text, `${fontSize}px`, fontFamily);
}, [container, text, fontSize, fontFamily]);
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useMemo, 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 [coords, setCoords] = useState({
x: 0,
y: 0,
height: 0,
width: 0,
});
const [forceRecalc, setForceRecalc] = useState(0);
const calculateCoords = useCallback(() => {
const coords = useMemo(() => {
const $page = document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
);
if (!$page) {
return;
return {
x: 0,
y: 0,
height: 0,
width: 0,
};
}
const { top, left, height, width } = getBoundingClientRect($page);
@@ -31,21 +31,17 @@ export const useFieldPageCoords = (field: Field) => {
const fieldHeight = (Number(field.height) / 100) * height;
const fieldWidth = (Number(field.width) / 100) * width;
setCoords({
return {
x: fieldX,
y: fieldY,
height: fieldHeight,
width: fieldWidth,
});
}, [field.height, field.page, field.positionX, field.positionY, field.width]);
useEffect(() => {
calculateCoords();
}, [calculateCoords]);
};
}, [field.height, field.page, field.positionX, field.positionY, field.width, forceRecalc]);
useEffect(() => {
const onResize = () => {
calculateCoords();
setForceRecalc((prev) => prev + 1);
};
window.addEventListener('resize', onResize);
@@ -53,7 +49,7 @@ export const useFieldPageCoords = (field: Field) => {
return () => {
window.removeEventListener('resize', onResize);
};
}, [calculateCoords]);
}, []);
useEffect(() => {
const $page = document.querySelector<HTMLElement>(
@@ -65,7 +61,7 @@ export const useFieldPageCoords = (field: Field) => {
}
const observer = new ResizeObserver(() => {
calculateCoords();
setForceRecalc((prev) => prev + 1);
});
observer.observe($page);
@@ -73,7 +69,7 @@ export const useFieldPageCoords = (field: Field) => {
return () => {
observer.disconnect();
};
}, [calculateCoords, field.page]);
}, [field.page]);
return coords;
};
@@ -1,11 +1,17 @@
import { useEffect, useState } from 'react';
import { useSyncExternalStore } from 'react';
const subscribe = () => {
return () => {};
};
const getSnapshot = () => {
return true;
};
const getServerSnapshot = () => {
return false;
};
export const useIsMounted = () => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted;
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
};
+1
View File
@@ -33,6 +33,7 @@
"@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",
+7
View File
@@ -0,0 +1,7 @@
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') ?? '',
});
@@ -111,7 +111,7 @@ export const getDocumentWhereInput = async ({
visibility: {
in: teamVisibilityFilters,
},
teamId,
teamId: team.id,
},
// Or, if they are a recipient of the document.
{
@@ -134,6 +134,9 @@ export const setDocumentRecipients = async ({
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
);
const canPersistedRecipientBeModified =
existing && canRecipientBeModified(existing, document.fields);
if (
existing &&
hasRecipientBeenChanged(existing, recipient) &&
@@ -147,6 +150,7 @@ export const setDocumentRecipients = async ({
return {
...recipient,
_persisted: existing,
canPersistedRecipientBeModified,
};
});
@@ -162,6 +166,13 @@ export const setDocumentRecipients = async ({
});
}
if (recipient._persisted && !recipient.canPersistedRecipientBeModified) {
return {
...recipient._persisted,
clientId: recipient.clientId,
};
}
const upsertedRecipient = await tx.recipient.upsert({
where: {
id: recipient._persisted?.id ?? -1,
@@ -1,7 +1,5 @@
import type { Recipient } from '@prisma/client';
import { RecipientRole } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
import { isDeepEqual } from 'remeda';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
@@ -104,10 +102,7 @@ export const updateDocumentRecipients = async ({
});
}
if (
hasRecipientBeenChanged(originalRecipient, recipient) &&
!canRecipientBeModified(originalRecipient, document.fields)
) {
if (!canRecipientBeModified(originalRecipient, document.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Cannot modify a recipient who has already interacted with the document',
});
@@ -203,9 +198,6 @@ export const updateDocumentRecipients = async ({
};
};
/**
* If you change this you MUST update the `hasRecipientBeenChanged` function.
*/
type RecipientData = {
id: number;
email?: string;
@@ -215,19 +207,3 @@ type RecipientData = {
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
};
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
const newRecipientActionAuth = newRecipientData.actionAuth || null;
return (
recipient.email !== newRecipientData.email ||
recipient.name !== newRecipientData.name ||
recipient.role !== newRecipientData.role ||
recipient.signingOrder !== newRecipientData.signingOrder ||
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
);
};
@@ -0,0 +1,72 @@
import { plainClient } from '@documenso/lib/plain/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildOrganisationWhereQuery } from '../../utils/organisations';
import { getTeamById } from '../team/get-team';
type SubmitSupportTicketOptions = {
subject: string;
message: string;
userId: number;
organisationId: string;
teamId?: number | null;
};
export const submitSupportTicket = async ({
subject,
message,
userId,
organisationId,
teamId,
}: SubmitSupportTicketOptions) => {
const user = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId,
userId,
}),
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
const team = teamId
? await getTeamById({
userId,
teamId,
})
: null;
const customMessage = `
Organisation: ${organisation.name} (${organisation.id})
Team: ${team ? `${team.name} (${team.id})` : 'No team provided'}
${message}`;
const res = await plainClient.createThread({
title: subject,
customerIdentifier: { emailAddress: user.email },
components: [{ componentText: { text: customMessage } }],
});
if (res.error) {
throw new Error(res.error.message);
}
return res;
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZResetTwoFactorRequestSchema,
ZResetTwoFactorResponseSchema,
} from './reset-two-factor-authentication.types';
export const resetTwoFactorRoute = adminProcedure
.input(ZResetTwoFactorRequestSchema)
.output(ZResetTwoFactorResponseSchema)
.mutation(async ({ input, ctx }) => {
const { userId } = input;
ctx.logger.info({
input: {
userId,
},
});
return await resetTwoFactor({ userId });
});
export type ResetTwoFactorOptions = {
userId: number;
};
export const resetTwoFactor = async ({ userId }: ResetTwoFactorOptions) => {
const user = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
}
await prisma.user.update({
where: {
id: user.id,
},
data: {
twoFactorEnabled: false,
twoFactorBackupCodes: null,
twoFactorSecret: null,
},
});
};
@@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZResetTwoFactorRequestSchema = z.object({
userId: z.number(),
});
export const ZResetTwoFactorResponseSchema = z.void();
export type TResetTwoFactorRequest = z.infer<typeof ZResetTwoFactorRequestSchema>;
export type TResetTwoFactorResponse = z.infer<typeof ZResetTwoFactorResponseSchema>;
@@ -21,6 +21,7 @@ import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
import { getAdminOrganisationRoute } from './get-admin-organisation';
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
import {
ZAdminDeleteDocumentMutationSchema,
ZAdminDeleteUserMutationSchema,
@@ -51,6 +52,9 @@ export const adminRouter = router({
stripe: {
createCustomer: createStripeCustomerRoute,
},
user: {
resetTwoFactor: resetTwoFactorRoute,
},
// Todo: migrate old routes
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
@@ -1,8 +1,10 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { submitSupportTicket } from '@documenso/lib/server-only/user/submit-support-ticket';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
@@ -10,6 +12,7 @@ import {
ZFindUserSecurityAuditLogsSchema,
ZRetrieveUserByIdQuerySchema,
ZSetProfileImageMutationSchema,
ZSubmitSupportTicketMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
@@ -91,4 +94,28 @@ export const profileRouter = router({
requestMetadata: ctx.metadata,
});
}),
submitSupportTicket: authenticatedProcedure
.input(ZSubmitSupportTicketMutationSchema)
.mutation(async ({ input, ctx }) => {
const { subject, message, organisationId, teamId } = input;
const userId = ctx.user.id;
const parsedTeamId = teamId ? Number(teamId) : null;
if (Number.isNaN(parsedTeamId)) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid team ID provided',
});
}
return await submitSupportTicket({
subject,
message,
userId,
organisationId,
teamId: parsedTeamId,
});
}),
});
@@ -27,3 +27,12 @@ export const ZSetProfileImageMutationSchema = z.object({
});
export type TSetProfileImageMutationSchema = z.infer<typeof ZSetProfileImageMutationSchema>;
export const ZSubmitSupportTicketMutationSchema = z.object({
organisationId: z.string(),
teamId: z.string().min(1).nullish(),
subject: z.string().min(3, 'Subject is required'),
message: z.string().min(10, 'Message must be at least 10 characters'),
});
export type TSupportTicketRequest = z.infer<typeof ZSubmitSupportTicketMutationSchema>;
+1
View File
@@ -141,6 +141,7 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next, path })
return await next({
ctx: {
...ctx,
teamId: ctx.teamId || -1,
logger: trpcSessionLogger,
user: ctx.user,
session: ctx.session,
+21 -9
View File
@@ -1,11 +1,23 @@
import { useEffect, useState } from 'react';
import { useSyncExternalStore } from 'react';
export const ClientOnly = async ({ children }: { children: React.ReactNode }) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? children : null;
const subscribe = () => {
return () => {};
};
const getSnapshot = () => {
return true;
};
const getServerSnapshot = () => {
return false;
};
export const ClientOnly = ({
children,
}: {
children: React.ReactNode;
}): React.ReactElement | null => {
const isClient = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isClient ? <>{children}</> : null;
};
@@ -18,7 +18,6 @@ import {
} from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { prop, sortBy } from 'remeda';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
@@ -103,6 +102,10 @@ export const AddFieldsFormPartial = ({
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
const handleMissingSignatureDialogOpenChange = (value: boolean) => {
setIsMissingSignatureDialogVisible(value);
};
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
const { currentStep, totalSteps, previousStep } = useStep();
const canRenderBackButtonAsRemove =
@@ -511,18 +514,24 @@ export const AddFieldsFormPartial = ({
};
}, []);
useEffect(() => {
const defaultSelectedSigner = useMemo(() => {
const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
setSelectedSigner(
return (
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
recipientsByRoleToDisplay[0],
recipientsByRoleToDisplay[0]
);
}, [recipients]);
if (selectedSigner && !recipients.find((r) => r.id === selectedSigner.id)) {
setSelectedSigner(defaultSelectedSigner);
} else if (!selectedSigner && defaultSelectedSigner) {
setSelectedSigner(defaultSelectedSigner);
}
const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
CC: [],
@@ -539,29 +548,6 @@ export const AddFieldsFormPartial = ({
return recipientsByRole;
}, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
.filter(
([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
role !== RecipientRole.ASSISTANT,
)
.map(
([role, roleRecipients]) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
[
role,
sortBy(
roleRecipients,
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
[prop('id'), 'asc'],
),
] as [RecipientRole, Recipient[]],
);
}, [recipientsByRole]);
const handleAdvancedSettings = () => {
setShowAdvancedSettings((prev) => !prev);
};
@@ -995,7 +981,7 @@ export const AddFieldsFormPartial = ({
<MissingSignatureFieldDialog
isOpen={isMissingSignatureDialogVisible}
onOpenChange={(value) => setIsMissingSignatureDialogVisible(value)}
onOpenChange={handleMissingSignatureDialogOpenChange}
/>
</>
)}
@@ -1,4 +1,4 @@
import { forwardRef, useEffect, useState } from 'react';
import { forwardRef, useMemo, useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
@@ -160,22 +160,32 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
const defaultState: FieldMeta = getDefaultState(field.type);
const [fieldState, setFieldState] = useState(() => {
const savedState = localStorage.getItem(localStorageKey);
return savedState ? { ...defaultState, ...JSON.parse(savedState) } : defaultState;
});
useEffect(() => {
const initialFieldState = useMemo(() => {
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
setFieldState({
return {
...defaultState,
...parsedFieldMeta,
});
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldMeta]);
const savedState = localStorage.getItem(localStorageKey);
return savedState ? { ...defaultState, ...JSON.parse(savedState) } : defaultState;
}, [fieldMeta, defaultState, localStorageKey]);
const [fieldState, setFieldState] = useState(initialFieldState);
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
const expectedState = {
...defaultState,
...parsedFieldMeta,
};
if (JSON.stringify(fieldState) !== JSON.stringify(expectedState)) {
setFieldState(expectedState);
}
}
const handleFieldChange = (
key: FieldMetaKeys,
@@ -1,12 +1,12 @@
import type { MouseEvent, PointerEvent, RefObject, TouchEvent } from 'react';
import { useMemo, useRef, useState } from 'react';
import { useLayoutEffect } from 'react';
import { Trans } from '@lingui/react/macro';
import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import {
SIGNATURE_CANVAS_DPI,
SIGNATURE_MIN_COVERAGE_THRESHOLD,
@@ -247,7 +247,7 @@ export const SignaturePadDraw = ({
onChange?.($el.current.toDataURL());
};
unsafe_useEffectOnce(() => {
useLayoutEffect(() => {
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
$el.current.height = $el.current.clientHeight * SIGNATURE_CANVAS_DPI;
@@ -270,7 +270,7 @@ export const SignaturePadDraw = ({
img.src = value;
}
});
}, [value]);
return (
<div className={cn('h-full w-full', className)}>
@@ -1,10 +1,9 @@
import { useRef } from 'react';
import { useLayoutEffect, useRef } from 'react';
import { Trans } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { UploadCloudIcon } from 'lucide-react';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { SIGNATURE_CANVAS_DPI } from '@documenso/lib/constants/signatures';
import { cn } from '../../lib/utils';
@@ -97,7 +96,7 @@ export const SignaturePadUpload = ({
}
};
unsafe_useEffectOnce(() => {
useLayoutEffect(() => {
// Todo: Not really sure if this is required for uploaded images.
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
@@ -121,7 +120,7 @@ export const SignaturePadUpload = ({
img.src = value;
}
});
}, [value]);
return (
<div className={cn('relative h-full w-full', className)}>
@@ -1,5 +1,3 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@@ -16,7 +14,7 @@ import {
DOCUMENT_SIGNATURE_TYPES,
} from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@@ -113,7 +111,8 @@ export const AddTemplateSettingsFormPartial = ({
meta: {
subject: template.templateMeta?.subject ?? '',
message: template.templateMeta?.message ?? '',
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
timezone:
template.templateMeta?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
dateFormat: (template.templateMeta?.dateFormat ??
DEFAULT_DOCUMENT_DATE_FORMAT) as TDocumentMetaDateFormat,
@@ -152,14 +151,6 @@ export const AddTemplateSettingsFormPartial = ({
)
.otherwise(() => false);
// We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed.
useEffect(() => {
if (!form.formState.touchedFields.meta?.timezone && !template.templateMeta?.timezone) {
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}, [form, form.setValue, form.formState.touchedFields.meta?.timezone]);
return (
<>
<DocumentFlowFormContainerHeader
+1
View File
@@ -47,6 +47,7 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
"NEXT_PUBLIC_DISABLE_SIGNUP",
"NEXT_PRIVATE_PLAIN_API_KEY",
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
"NEXT_PRIVATE_DATABASE_URL",
"NEXT_PRIVATE_DIRECT_DATABASE_URL",