Compare commits

..

1 Commits

Author SHA1 Message Date
David Nguyen e7e2aa9bd8 fix: migrate template metadata 2025-08-21 17:56:04 +10:00
70 changed files with 736 additions and 707 deletions
@@ -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">
@@ -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>
@@ -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>
@@ -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
@@ -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>
@@ -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>
@@ -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>
@@ -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(
@@ -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">
@@ -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">
@@ -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">
@@ -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">
@@ -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>
@@ -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">
@@ -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">
@@ -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">
@@ -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}>
@@ -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">
@@ -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">
@@ -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">
@@ -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">
@@ -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.
>
@@ -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">
@@ -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';
@@ -119,17 +119,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>
@@ -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">
@@ -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 })
@@ -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>
@@ -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';
@@ -203,17 +203,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">
@@ -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">
@@ -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">
@@ -3,7 +3,7 @@ import { useEffect, useLayoutEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@prisma/client';
import type { DocumentMeta, Recipient, Signature } from '@prisma/client';
import { type DocumentData, type Field, FieldType } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon';
@@ -48,7 +48,7 @@ export type EmbedDirectTemplateClientPageProps = {
documentData: DocumentData;
recipient: Recipient;
fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null;
metadata?: DocumentMeta | null;
hidePoweredBy?: boolean;
allowWhiteLabelling?: boolean;
};
@@ -1,4 +1,4 @@
import type { DocumentMeta, TemplateMeta } from '@prisma/client';
import type { DocumentMeta } from '@prisma/client';
import { type Field, FieldType } from '@prisma/client';
import { match } from 'ts-pattern';
@@ -33,7 +33,7 @@ import { DocumentSigningTextField } from '~/components/general/document-signing/
export type EmbedDocumentFieldsProps = {
fields: Field[];
metadata?: Pick<
DocumentMeta | TemplateMeta,
DocumentMeta,
| 'timezone'
| 'dateFormat'
| 'typedSignatureEnabled'
@@ -3,7 +3,7 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, TemplateMeta } from '@prisma/client';
import type { DocumentMeta } from '@prisma/client';
import {
type DocumentData,
type Field,
@@ -50,7 +50,7 @@ export type EmbedSignDocumentClientPageProps = {
recipient: RecipientWithFields;
fields: Field[];
completedFields: DocumentField[];
metadata?: DocumentMeta | TemplateMeta | null;
metadata?: DocumentMeta | null;
isCompleted?: boolean;
hidePoweredBy?: boolean;
allowWhitelabelling?: boolean;
@@ -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">
@@ -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"
+14 -11
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
+14 -11
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">
@@ -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>
@@ -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,12 +216,14 @@ 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;
@@ -312,8 +318,7 @@ export const DocumentSigningTextField = ({
className="flex-1"
onClick={() => {
setShowCustomTextModal(false);
setLocalCustomText(parsedFieldMeta?.text ?? '');
setErrors(initialErrors);
setLocalCustomText('');
}}
>
<Trans>Cancel</Trans>
@@ -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
@@ -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">
@@ -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>
@@ -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>
@@ -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;
}
@@ -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">
@@ -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>
@@ -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.`}
@@ -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) {
+6
View File
@@ -554,6 +554,12 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
status: 200,
body: {
...template,
templateMeta: template.templateMeta
? {
...template.templateMeta,
templateId: template.id,
}
: null,
Field: template.fields.map((field) => ({
...field,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
@@ -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;
};
@@ -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;
}
@@ -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;
};
@@ -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;
};
@@ -1,4 +1,4 @@
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
import type { DocumentMeta, DocumentVisibility } from '@prisma/client';
import {
DocumentSource,
RecipientRole,
@@ -46,7 +46,7 @@ export type CreateDocumentOptions = {
formValues?: TDocumentFormValues;
recipients: TCreateDocumentV2Request['recipients'];
};
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
requestMetadata: ApiRequestMetadata;
};
@@ -1,13 +0,0 @@
import { prisma } from '@documenso/prisma';
export interface GetDocumentMetaByDocumentIdOptions {
id: number;
}
export const getDocumentMetaByDocumentId = async ({ id }: GetDocumentMetaByDocumentIdOptions) => {
return await prisma.documentMeta.findFirstOrThrow({
where: {
documentId: id,
},
});
};
@@ -1,4 +1,4 @@
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
@@ -26,7 +26,7 @@ export type CreateTemplateOptions = {
publicDescription?: string;
type?: Template['type'];
};
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
};
export const ZCreateTemplateResponseSchema = TemplateSchema;
@@ -1,4 +1,4 @@
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
import { prisma } from '@documenso/prisma';
@@ -22,7 +22,7 @@ export type UpdateTemplateOptions = {
type?: Template['type'];
useLegacyFieldInsertion?: boolean;
};
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
};
export const updateTemplate = async ({
+3 -3
View File
@@ -1,10 +1,10 @@
import type { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
import { TemplateMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateMetaSchema';
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
@@ -39,7 +39,7 @@ export const ZTemplateSchema = TemplateSchema.pick({
data: true,
initialData: true,
}),
templateMeta: TemplateMetaSchema.pick({
templateMeta: DocumentMetaSchema.pick({
id: true,
subject: true,
message: true,
@@ -129,7 +129,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({
}).nullable(),
fields: ZFieldSchema.array(),
recipients: ZRecipientLiteSchema.array(),
templateMeta: TemplateMetaSchema.pick({
templateMeta: DocumentMetaSchema.pick({
signingOrder: true,
distributionMethod: true,
}).nullable(),
+3 -8
View File
@@ -1,9 +1,4 @@
import type {
Document,
DocumentMeta,
OrganisationGlobalSettings,
TemplateMeta,
} from '@prisma/client';
import type { Document, DocumentMeta, OrganisationGlobalSettings } from '@prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
@@ -29,7 +24,7 @@ export const isDocumentCompleted = (document: Pick<Document, 'status'> | Documen
*/
export const extractDerivedDocumentMeta = (
settings: Omit<OrganisationGlobalSettings, 'id'>,
overrideMeta: Partial<DocumentMeta | TemplateMeta> | undefined | null,
overrideMeta: Partial<DocumentMeta> | undefined | null,
) => {
const meta = overrideMeta ?? {};
@@ -58,5 +53,5 @@ export const extractDerivedDocumentMeta = (
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
emailSettings:
meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
} satisfies Omit<DocumentMeta, 'id' | 'documentId'>;
} satisfies Omit<DocumentMeta, 'id' | 'documentId' | 'templateId'>;
};
@@ -0,0 +1,57 @@
-- DropForeignKey
ALTER TABLE "TemplateMeta" DROP CONSTRAINT "TemplateMeta_templateId_fkey";
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "templateId" INTEGER,
ALTER COLUMN "documentId" DROP NOT NULL;
-- [CUSTOM_CHANGE] Migrate existing TemplateMeta to DocumentMeta
INSERT INTO "DocumentMeta" (
"id",
"subject",
"message",
"timezone",
"password",
"dateFormat",
"redirectUrl",
"signingOrder",
"allowDictateNextSigner",
"typedSignatureEnabled",
"uploadSignatureEnabled",
"drawSignatureEnabled",
"language",
"distributionMethod",
"emailSettings",
"emailReplyTo",
"emailId",
"templateId"
)
SELECT
gen_random_uuid()::text, -- Generate new CUID-like IDs to avoid collisions
"subject",
"message",
"timezone",
"password",
"dateFormat",
"redirectUrl",
"signingOrder",
"allowDictateNextSigner",
"typedSignatureEnabled",
"uploadSignatureEnabled",
"drawSignatureEnabled",
"language",
"distributionMethod",
"emailSettings",
"emailReplyTo",
"emailId",
"templateId"
FROM "TemplateMeta";
-- DropTable
DROP TABLE "TemplateMeta";
-- CreateIndex
CREATE UNIQUE INDEX "DocumentMeta_templateId_key" ON "DocumentMeta"("templateId");
-- AddForeignKey
ALTER TABLE "DocumentMeta" ADD CONSTRAINT "DocumentMeta_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+7 -29
View File
@@ -456,8 +456,6 @@ model DocumentMeta {
timezone String? @default("Etc/UTC") @db.Text
password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String?
signingOrder DocumentSigningOrder @default(PARALLEL)
allowDictateNextSigner Boolean @default(false)
@@ -472,6 +470,12 @@ model DocumentMeta {
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
emailReplyTo String?
emailId String?
documentId Int? @unique
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
templateId Int? @unique
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
}
enum ReadStatus {
@@ -842,32 +846,6 @@ enum TemplateType {
PRIVATE
}
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
model TemplateMeta {
id String @id @default(cuid())
subject String?
message String?
timezone String? @default("Etc/UTC") @db.Text
password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
signingOrder DocumentSigningOrder? @default(PARALLEL)
allowDictateNextSigner Boolean @default(false)
distributionMethod DocumentDistributionMethod @default(EMAIL)
typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true)
templateId Int @unique
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
redirectUrl String?
language String @default("en")
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
emailReplyTo String?
emailId String?
}
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
model Template {
id Int @id @default(autoincrement())
@@ -876,7 +854,7 @@ model Template {
title String
visibility DocumentVisibility @default(EVERYONE)
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
templateMeta TemplateMeta?
templateMeta DocumentMeta?
templateDocumentDataId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@ -94,7 +94,7 @@ export const createEmbeddingTemplateRoute = procedure
emailSettings: meta.emailSettings,
};
await prisma.templateMeta.upsert({
await prisma.documentMeta.upsert({
where: {
templateId: template.id,
},
+9 -21
View File
@@ -1,23 +1,11 @@
import { useSyncExternalStore } from 'react';
import { useEffect, useState } from 'react';
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;
export const ClientOnly = async ({ children }: { children: React.ReactNode }) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? children : null;
};
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Field, Recipient, TemplateMeta } from '@prisma/client';
import type { DocumentMeta, Field, Recipient } from '@prisma/client';
import { SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react';
@@ -36,7 +36,7 @@ const getRecipientDisplayText = (recipient: { name: string; email: string }) =>
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
documentMeta?: Pick<DocumentMeta, 'dateFormat'>;
showFieldStatus?: boolean;
@@ -18,6 +18,7 @@ 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';
@@ -102,10 +103,6 @@ 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 =
@@ -514,24 +511,18 @@ export const AddFieldsFormPartial = ({
};
}, []);
const defaultSelectedSigner = useMemo(() => {
useEffect(() => {
const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
return (
setSelectedSigner(
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: [],
@@ -548,6 +539,29 @@ 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);
};
@@ -981,7 +995,7 @@ export const AddFieldsFormPartial = ({
<MissingSignatureFieldDialog
isOpen={isMissingSignatureDialogVisible}
onOpenChange={handleMissingSignatureDialogOpenChange}
onOpenChange={(value) => setIsMissingSignatureDialogVisible(value)}
/>
</>
)}
@@ -1,5 +1,5 @@
import { useLingui } from '@lingui/react';
import type { DocumentMeta, Signature, TemplateMeta } from '@prisma/client';
import type { DocumentMeta, Signature } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { ChevronDown } from 'lucide-react';
@@ -27,7 +27,7 @@ type FieldIconProps = {
fieldMeta?: TFieldMetaSchema | null;
signature?: Signature | null;
};
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
documentMeta?: Pick<DocumentMeta, 'dateFormat'>;
};
/**
@@ -1,4 +1,4 @@
import { forwardRef, useMemo, useState } from 'react';
import { forwardRef, useEffect, useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
@@ -160,32 +160,22 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
const defaultState: FieldMeta = getDefaultState(field.type);
const initialFieldState = useMemo(() => {
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
return {
...defaultState,
...parsedFieldMeta,
};
}
const [fieldState, setFieldState] = useState(() => {
const savedState = localStorage.getItem(localStorageKey);
return savedState ? { ...defaultState, ...JSON.parse(savedState) } : defaultState;
}, [fieldMeta, defaultState, localStorageKey]);
});
const [fieldState, setFieldState] = useState(initialFieldState);
useEffect(() => {
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
const expectedState = {
...defaultState,
...parsedFieldMeta,
};
if (JSON.stringify(fieldState) !== JSON.stringify(expectedState)) {
setFieldState(expectedState);
setFieldState({
...defaultState,
...parsedFieldMeta,
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldMeta]);
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());
};
useLayoutEffect(() => {
unsafe_useEffectOnce(() => {
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,9 +1,10 @@
import { useLayoutEffect, useRef } from 'react';
import { 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';
@@ -96,7 +97,7 @@ export const SignaturePadUpload = ({
}
};
useLayoutEffect(() => {
unsafe_useEffectOnce(() => {
// Todo: Not really sure if this is required for uploaded images.
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
@@ -120,7 +121,7 @@ export const SignaturePadUpload = ({
img.src = value;
}
}, [value]);
});
return (
<div className={cn('relative h-full w-full', className)}>
@@ -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';
@@ -14,7 +16,7 @@ import {
DOCUMENT_SIGNATURE_TYPES,
} from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { DEFAULT_DOCUMENT_TIME_ZONE, 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';
@@ -111,8 +113,7 @@ export const AddTemplateSettingsFormPartial = ({
meta: {
subject: template.templateMeta?.subject ?? '',
message: template.templateMeta?.message ?? '',
timezone:
template.templateMeta?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
dateFormat: (template.templateMeta?.dateFormat ??
DEFAULT_DOCUMENT_DATE_FORMAT) as TDocumentMetaDateFormat,
@@ -151,6 +152,14 @@ 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