mirror of
https://github.com/documenso/documenso.git
synced 2026-07-06 11:05:00 +10:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1464ac2d3 | |||
| e7e2aa9bd8 |
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
+20
-15
@@ -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">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { Document, Role, Subscription } from '@prisma/client';
|
||||
import type { Role, Subscription } from '@prisma/client';
|
||||
import { Edit, Loader } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
@@ -20,7 +20,7 @@ type UserData = {
|
||||
email: string;
|
||||
roles: Role[];
|
||||
subscriptions?: SubscriptionLite[] | null;
|
||||
documents: DocumentLite[];
|
||||
documentCount: number;
|
||||
};
|
||||
|
||||
type SubscriptionLite = Pick<
|
||||
@@ -28,8 +28,6 @@ type SubscriptionLite = Pick<
|
||||
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
|
||||
>;
|
||||
|
||||
type DocumentLite = Pick<Document, 'id'>;
|
||||
|
||||
type AdminDashboardUsersTableProps = {
|
||||
users: UserData[];
|
||||
totalPages: number;
|
||||
@@ -74,10 +72,7 @@ export const AdminDashboardUsersTable = ({
|
||||
},
|
||||
{
|
||||
header: _(msg`Documents`),
|
||||
accessorKey: 'documents',
|
||||
cell: ({ row }) => {
|
||||
return <div>{row.original.documents?.length}</div>;
|
||||
},
|
||||
accessorKey: 'documentCount',
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
|
||||
@@ -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>
|
||||
|
||||
+8
-1
@@ -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) {
|
||||
|
||||
@@ -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,39 +1,32 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type GetDocumentByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const getDocumentById = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumentByIdOptions) => {
|
||||
const { documentWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
validatedUserId: userId,
|
||||
unvalidatedTeamId: teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
folderId,
|
||||
},
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
documentData: true,
|
||||
documents: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
@@ -56,7 +49,7 @@ export const getDocumentById = async ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document could not be found',
|
||||
});
|
||||
@@ -64,93 +57,3 @@ export const getDocumentById = async ({
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
export type GetDocumentWhereInputOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a given Prisma document query.
|
||||
*
|
||||
* This will return a query that allows a user to get a document if they have valid access to it.
|
||||
*/
|
||||
export const getDocumentWhereInput = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetDocumentWhereInputOptions) => {
|
||||
const team = await getTeamById({ teamId, userId });
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const teamVisibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
])
|
||||
.otherwise(() => [DocumentVisibility.EVERYONE]);
|
||||
|
||||
const documentOrInput: Prisma.DocumentWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
{
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Allow access to documents sent to or from the team email.
|
||||
if (team.teamEmail) {
|
||||
documentOrInput.push(
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const documentWhereInput: Prisma.DocumentWhereUniqueInput = {
|
||||
id: documentId,
|
||||
OR: documentOrInput,
|
||||
};
|
||||
|
||||
return {
|
||||
documentWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { buildEnvelopeIdQuery } from '../../utils/envelope';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetEnvelopeByIdOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const getEnvelopeById = async ({ id, userId, teamId }: GetEnvelopeByIdOptions) => {
|
||||
const { documentWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
validatedUserId: userId,
|
||||
unvalidatedTeamId: teamId,
|
||||
});
|
||||
|
||||
const document = await prisma.envelope.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
documents: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document could not be found',
|
||||
});
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
export type GetEnvelopeWhereInputOptions = {
|
||||
id: EnvelopeIdOptions;
|
||||
|
||||
/**
|
||||
* The user ID who has been authenticated.
|
||||
*/
|
||||
validatedUserId: number;
|
||||
|
||||
/**
|
||||
* The unknown teamId from the request.
|
||||
*/
|
||||
unvalidatedTeamId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the where input for a given Prisma envelope query.
|
||||
*
|
||||
* This will return a query that allows a user to get a document if they have valid access to it.
|
||||
*/
|
||||
export const getEnvelopeWhereInput = async ({
|
||||
id,
|
||||
validatedUserId,
|
||||
unvalidatedTeamId,
|
||||
}: GetEnvelopeWhereInputOptions) => {
|
||||
const team = await getTeamById({ teamId: unvalidatedTeamId, userId: validatedUserId });
|
||||
|
||||
const teamVisibilityFilters = match(team.currentTeamRole)
|
||||
.with(TeamMemberRole.ADMIN, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
DocumentVisibility.ADMIN,
|
||||
])
|
||||
.with(TeamMemberRole.MANAGER, () => [
|
||||
DocumentVisibility.EVERYONE,
|
||||
DocumentVisibility.MANAGER_AND_ABOVE,
|
||||
])
|
||||
.otherwise(() => [DocumentVisibility.EVERYONE]);
|
||||
|
||||
const documentOrInput: Prisma.EnvelopeWhereInput[] = [
|
||||
// Allow access if they own the document.
|
||||
{
|
||||
userId: validatedUserId,
|
||||
},
|
||||
// Or, if they belong to the team that the document is associated with.
|
||||
{
|
||||
visibility: {
|
||||
in: teamVisibilityFilters,
|
||||
},
|
||||
teamId: team.id,
|
||||
},
|
||||
// Or, if they are a recipient of the document.
|
||||
// ????????????? should recipients be able to do X?
|
||||
// {
|
||||
// status: {
|
||||
// not: DocumentStatus.DRAFT,
|
||||
// },
|
||||
// recipients: {
|
||||
// some: {
|
||||
// email: user.email,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
// Allow access to documents sent to or from the team email.
|
||||
if (team.teamEmail) {
|
||||
documentOrInput.push(
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const documentWhereInput: Prisma.EnvelopeWhereUniqueInput = {
|
||||
...buildEnvelopeIdQuery(id),
|
||||
OR: documentOrInput,
|
||||
};
|
||||
|
||||
return {
|
||||
documentWhereInput,
|
||||
team,
|
||||
};
|
||||
};
|
||||
@@ -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 ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@@ -25,9 +25,10 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
|
||||
const serviceAccount = await deletedAccountServiceAccount();
|
||||
|
||||
// TODO: Send out cancellations for all pending docs
|
||||
await prisma.document.updateMany({
|
||||
await prisma.envelope.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: {
|
||||
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { EnvelopeType, Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@@ -34,12 +34,20 @@ export const findUsers = async ({
|
||||
|
||||
const [users, count] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
include: {
|
||||
documents: {
|
||||
select: {
|
||||
_count: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopes: {
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
roles: true,
|
||||
},
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
@@ -51,7 +59,10 @@ export const findUsers = async ({
|
||||
]);
|
||||
|
||||
return {
|
||||
users,
|
||||
users: users.map((user) => ({
|
||||
...user,
|
||||
documentCount: user._count.envelopes,
|
||||
})),
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { z } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import { DocumentSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentSchema';
|
||||
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
import { LegacyDocumentSchema } from '@documenso/prisma/types/document-legacy-schema';
|
||||
|
||||
import { ZFieldSchema } from './field';
|
||||
import { ZRecipientLiteSchema } from './recipient';
|
||||
@@ -15,7 +15,7 @@ import { ZRecipientLiteSchema } from './recipient';
|
||||
*
|
||||
* Mainly used for returning a single document from the API.
|
||||
*/
|
||||
export const ZDocumentSchema = DocumentSchema.pick({
|
||||
export const ZDocumentSchema = LegacyDocumentSchema.pick({
|
||||
visibility: true,
|
||||
status: true,
|
||||
source: true,
|
||||
@@ -31,9 +31,12 @@ export const ZDocumentSchema = DocumentSchema.pick({
|
||||
completedAt: true,
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
folderId: true,
|
||||
}).extend({
|
||||
// Which "Template" the document was created from. Legacy field for backwards compatibility.
|
||||
// The actual field is now called `createdFromDocumentId`.
|
||||
templateId: z.number().optional(),
|
||||
|
||||
// Todo: Maybe we want to alter this a bit since this returns a lot of data.
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
@@ -82,7 +85,7 @@ export type TDocument = z.infer<typeof ZDocumentSchema>;
|
||||
/**
|
||||
* A lite version of the document response schema without relations.
|
||||
*/
|
||||
export const ZDocumentLiteSchema = DocumentSchema.pick({
|
||||
export const ZDocumentLiteSchema = LegacyDocumentSchema.pick({
|
||||
visibility: true,
|
||||
status: true,
|
||||
source: true,
|
||||
@@ -98,9 +101,12 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
|
||||
completedAt: true,
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
folderId: true,
|
||||
useLegacyFieldInsertion: true,
|
||||
}).extend({
|
||||
// Which "Template" the document was created from. Legacy field for backwards compatibility.
|
||||
// The actual field is now called `createdFromDocumentId`.
|
||||
templateId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>;
|
||||
@@ -108,7 +114,7 @@ export type TDocumentLite = z.infer<typeof ZDocumentLiteSchema>;
|
||||
/**
|
||||
* A version of the document response schema when returning multiple documents at once from a single API endpoint.
|
||||
*/
|
||||
export const ZDocumentManySchema = DocumentSchema.pick({
|
||||
export const ZDocumentManySchema = LegacyDocumentSchema.pick({
|
||||
visibility: true,
|
||||
status: true,
|
||||
source: true,
|
||||
@@ -124,10 +130,13 @@ export const ZDocumentManySchema = DocumentSchema.pick({
|
||||
completedAt: true,
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
folderId: true,
|
||||
useLegacyFieldInsertion: true,
|
||||
}).extend({
|
||||
// Which "Template" the document was created from. Legacy field for backwards compatibility.
|
||||
// The actual field is now called `createdFromDocumentId`.
|
||||
templateId: z.number().optional(),
|
||||
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -18,8 +18,6 @@ export const ZFieldSchema = FieldSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
secondaryId: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
recipientId: true,
|
||||
page: true,
|
||||
positionX: true,
|
||||
@@ -29,6 +27,10 @@ export const ZFieldSchema = FieldSchema.pick({
|
||||
customText: true,
|
||||
inserted: true,
|
||||
fieldMeta: true,
|
||||
}).extend({
|
||||
// Todo: Decide whether to make these two IDs backwards compatible.
|
||||
documentId: z.number().optional(),
|
||||
templateId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZFieldPageNumberSchema = z
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
@@ -15,8 +17,6 @@ export const ZRecipientSchema = RecipientSchema.pick({
|
||||
signingStatus: true,
|
||||
sendStatus: true,
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
token: true,
|
||||
@@ -28,6 +28,10 @@ export const ZRecipientSchema = RecipientSchema.pick({
|
||||
rejectionReason: true,
|
||||
}).extend({
|
||||
fields: ZFieldSchema.array(),
|
||||
|
||||
// Todo: Decide whether to make these two IDs backwards compatible.
|
||||
documentId: z.number().optional(),
|
||||
templateId: z.number().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -39,8 +43,6 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({
|
||||
signingStatus: true,
|
||||
sendStatus: true,
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
token: true,
|
||||
@@ -50,6 +52,10 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({
|
||||
authOptions: true,
|
||||
signingOrder: true,
|
||||
rejectionReason: true,
|
||||
}).extend({
|
||||
// Todo: Decide whether to make these two IDs backwards compatible.
|
||||
documentId: z.number().optional(),
|
||||
templateId: z.number().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -61,8 +67,6 @@ export const ZRecipientManySchema = RecipientSchema.pick({
|
||||
signingStatus: true,
|
||||
sendStatus: true,
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
token: true,
|
||||
@@ -83,4 +87,8 @@ export const ZRecipientManySchema = RecipientSchema.pick({
|
||||
id: true,
|
||||
url: true,
|
||||
}).nullable(),
|
||||
|
||||
// Todo: Decide whether to make these two IDs backwards compatible.
|
||||
documentId: z.number().optional(),
|
||||
templateId: z.number().optional(),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { z } from 'zod';
|
||||
import { 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';
|
||||
import { TemplateSchema } from '@documenso/prisma/types/template-legacy-schema';
|
||||
|
||||
import { ZFieldSchema } from './field';
|
||||
import { ZRecipientLiteSchema } from './recipient';
|
||||
@@ -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,
|
||||
@@ -51,13 +51,17 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
drawSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
distributionMethod: true,
|
||||
templateId: true,
|
||||
redirectUrl: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
emailId: true,
|
||||
emailReplyTo: true,
|
||||
}).nullable(),
|
||||
})
|
||||
.extend({
|
||||
// Legacy field for backwards compatibility. Needs to refer to the Envelope `secondaryTemplateId`.
|
||||
templateId: z.number(),
|
||||
})
|
||||
.nullable(),
|
||||
directLink: TemplateDirectLinkSchema.nullable(),
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
@@ -129,7 +133,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({
|
||||
}).nullable(),
|
||||
fields: ZFieldSchema.array(),
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
templateMeta: TemplateMetaSchema.pick({
|
||||
templateMeta: DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
}).nullable(),
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import type {
|
||||
Document,
|
||||
DocumentMeta,
|
||||
OrganisationGlobalSettings,
|
||||
TemplateMeta,
|
||||
} from '@prisma/client';
|
||||
import type { DocumentMeta, Envelope, OrganisationGlobalSettings } from '@prisma/client';
|
||||
import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
|
||||
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
|
||||
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
|
||||
|
||||
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
||||
export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | DocumentStatus) => {
|
||||
const status = typeof document === 'string' ? document : document.status;
|
||||
|
||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||
@@ -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' | 'envelopeId'>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppError, AppErrorCode } from '../errors/app-error';
|
||||
|
||||
const envelopeDocumentPrefixId = 'document';
|
||||
const envelopeTemplatePrefixId = 'template';
|
||||
const envelopePrefixId = 'envelope';
|
||||
|
||||
const ZDocumentIdSchema = z.string().regex(/^document_\d+$/);
|
||||
const ZTemplateIdSchema = z.string().regex(/^template_\d+$/);
|
||||
const ZEnvelopeIdSchema = z.string().regex(/^envelope_\d+$/);
|
||||
|
||||
export type EnvelopeIdOptions =
|
||||
| {
|
||||
type: 'envelopeId';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'documentId';
|
||||
id: string | number;
|
||||
}
|
||||
| {
|
||||
type: 'templateId';
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an unknown document or template ID.
|
||||
*
|
||||
* @param id
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
export const buildEnvelopeIdQuery = (options: EnvelopeIdOptions) => {
|
||||
return match(options)
|
||||
.with({ type: 'envelopeId' }, (value) => {
|
||||
const parsed = ZEnvelopeIdSchema.safeParse(value.id);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid envelope ID',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: value.id,
|
||||
};
|
||||
})
|
||||
.with({ type: 'documentId' }, (value) => ({
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
secondaryId: parseDocumentIdToEnvelopeSecondaryId(value.id),
|
||||
}))
|
||||
.with({ type: 'templateId' }, (value) => ({
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
secondaryId: parseTemplateIdToEnvelopeSecondaryId(value.id),
|
||||
}))
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
export const parseDocumentIdToEnvelopeSecondaryId = (documentId: string | number) => {
|
||||
if (typeof documentId === 'number') {
|
||||
return `${envelopeDocumentPrefixId}_${documentId}`;
|
||||
}
|
||||
|
||||
const parsed = ZDocumentIdSchema.safeParse(documentId);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid document ID',
|
||||
});
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
export const parseTemplateIdToEnvelopeSecondaryId = (templateId: string | number) => {
|
||||
if (typeof templateId === 'number') {
|
||||
return `${envelopeTemplatePrefixId}_${templateId}`;
|
||||
}
|
||||
|
||||
const parsed = ZTemplateIdSchema.safeParse(templateId);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid template ID',
|
||||
});
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { type Recipient } from '@prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `authOptions` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `completedAt` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `createdAt` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `deletedAt` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `externalId` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `folderId` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `formValues` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `source` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `status` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `teamId` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `templateId` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `updatedAt` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `useLegacyFieldInsertion` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `userId` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `visibility` on the `Document` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `documentId` on the `DocumentAuditLog` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `documentId` on the `DocumentMeta` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `templateId` on the `DocumentMeta` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `documentId` on the `DocumentShareLink` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `documentId` on the `Field` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `templateId` on the `Field` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `documentId` on the `Recipient` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `templateId` on the `Recipient` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `templateId` on the `TemplateDirectLink` table. All the data in the column will be lost.
|
||||
- You are about to drop the `Template` table. If the table is not empty, all the data it contains will be lost.
|
||||
- A unique constraint covering the columns `[envelopeId,email]` on the table `DocumentShareLink` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[envelopeId,email]` on the table `Recipient` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[envelopeId]` on the table `TemplateDirectLink` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `envelopeId` to the `DocumentAuditLog` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `envelopeId` to the `DocumentShareLink` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `envelopeId` to the `Field` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `envelopeId` to the `Recipient` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `envelopeId` to the `TemplateDirectLink` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `type` to the `TemplateDirectLink` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EnvelopeType" AS ENUM ('DOCUMENT', 'TEMPLATE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TemplateDirectLinkType" AS ENUM ('PUBLIC', 'PRIVATE');
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_folderId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_teamId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_templateId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "DocumentAuditLog" DROP CONSTRAINT "DocumentAuditLog_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "DocumentMeta" DROP CONSTRAINT "DocumentMeta_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "DocumentMeta" DROP CONSTRAINT "DocumentMeta_templateId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "DocumentShareLink" DROP CONSTRAINT "DocumentShareLink_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Field" DROP CONSTRAINT "Field_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Field" DROP CONSTRAINT "Field_templateId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Recipient" DROP CONSTRAINT "Recipient_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Recipient" DROP CONSTRAINT "Recipient_templateId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Template" DROP CONSTRAINT "Template_folderId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Template" DROP CONSTRAINT "Template_teamId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Template" DROP CONSTRAINT "Template_templateDocumentDataId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Template" DROP CONSTRAINT "Template_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "TemplateDirectLink" DROP CONSTRAINT "TemplateDirectLink_templateId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Document_folderId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Document_status_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Document_userId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "DocumentMeta_documentId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "DocumentMeta_templateId_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "DocumentShareLink_documentId_email_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Field_documentId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Field_templateId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Recipient_documentId_email_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Recipient_documentId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Recipient_templateId_email_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "Recipient_templateId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "TemplateDirectLink_templateId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" DROP COLUMN "authOptions",
|
||||
DROP COLUMN "completedAt",
|
||||
DROP COLUMN "createdAt",
|
||||
DROP COLUMN "deletedAt",
|
||||
DROP COLUMN "externalId",
|
||||
DROP COLUMN "folderId",
|
||||
DROP COLUMN "formValues",
|
||||
DROP COLUMN "source",
|
||||
DROP COLUMN "status",
|
||||
DROP COLUMN "teamId",
|
||||
DROP COLUMN "templateId",
|
||||
DROP COLUMN "updatedAt",
|
||||
DROP COLUMN "useLegacyFieldInsertion",
|
||||
DROP COLUMN "userId",
|
||||
DROP COLUMN "visibility";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentAuditLog" DROP COLUMN "documentId",
|
||||
ADD COLUMN "envelopeId" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentMeta" DROP COLUMN "documentId",
|
||||
DROP COLUMN "templateId",
|
||||
ADD COLUMN "envelopeId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentShareLink" DROP COLUMN "documentId",
|
||||
ADD COLUMN "envelopeId" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Field" DROP COLUMN "documentId",
|
||||
DROP COLUMN "templateId",
|
||||
ADD COLUMN "envelopeId" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Recipient" DROP COLUMN "documentId",
|
||||
DROP COLUMN "templateId",
|
||||
ADD COLUMN "envelopeId" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "TemplateDirectLink" DROP COLUMN "templateId",
|
||||
ADD COLUMN "envelopeId" TEXT NOT NULL,
|
||||
ADD COLUMN "type" "TemplateDirectLinkType" NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Template";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "TemplateType";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Envelope" (
|
||||
"id" TEXT NOT NULL,
|
||||
"secondaryId" TEXT NOT NULL,
|
||||
"externalId" TEXT,
|
||||
"type" "EnvelopeType" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"title" TEXT NOT NULL,
|
||||
"status" "DocumentStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"source" "DocumentSource" NOT NULL,
|
||||
"useLegacyFieldInsertion" BOOLEAN NOT NULL DEFAULT false,
|
||||
"authOptions" JSONB,
|
||||
"formValues" JSONB,
|
||||
"visibility" "DocumentVisibility" NOT NULL DEFAULT 'EVERYONE',
|
||||
"publicTitle" TEXT NOT NULL DEFAULT '',
|
||||
"publicDescription" TEXT NOT NULL DEFAULT '',
|
||||
"templateId" INTEGER,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"teamId" INTEGER NOT NULL,
|
||||
"folderId" TEXT,
|
||||
"documentMetaId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Envelope_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Envelope_secondaryId_key" ON "Envelope"("secondaryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Envelope_documentMetaId_key" ON "Envelope"("documentMetaId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "DocumentShareLink_envelopeId_email_key" ON "DocumentShareLink"("envelopeId", "email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Field_envelopeId_idx" ON "Field"("envelopeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Recipient_envelopeId_idx" ON "Recipient"("envelopeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Recipient_envelopeId_email_key" ON "Recipient"("envelopeId", "email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TemplateDirectLink_envelopeId_key" ON "TemplateDirectLink"("envelopeId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Envelope" ADD CONSTRAINT "Envelope_documentMetaId_fkey" FOREIGN KEY ("documentMetaId") REFERENCES "DocumentMeta"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DocumentAuditLog" ADD CONSTRAINT "DocumentAuditLog_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Field" ADD CONSTRAINT "Field_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "DocumentShareLink" ADD CONSTRAINT "DocumentShareLink_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TemplateDirectLink" ADD CONSTRAINT "TemplateDirectLink_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `envelopeId` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" ADD COLUMN "envelopeId" TEXT NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_envelopeId_fkey" FOREIGN KEY ("envelopeId") REFERENCES "Envelope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+86
-128
@@ -64,8 +64,7 @@ model User {
|
||||
twoFactorBackupCodes String?
|
||||
|
||||
folders Folder[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
envelopes Envelope[]
|
||||
|
||||
verificationTokens VerificationToken[]
|
||||
apiTokens ApiToken[]
|
||||
@@ -348,8 +347,7 @@ model Folder {
|
||||
pinned Boolean @default(false)
|
||||
parentId String?
|
||||
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
envelopes Envelope[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
subfolders Folder[] @relation("FolderToFolder")
|
||||
@@ -362,53 +360,79 @@ model Folder {
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.")
|
||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||
enum EnvelopeType {
|
||||
DOCUMENT
|
||||
TEMPLATE
|
||||
}
|
||||
|
||||
model Envelope {
|
||||
id String @id @default(cuid())
|
||||
secondaryId String @unique
|
||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||
|
||||
type EnvelopeType
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
completedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
|
||||
title String
|
||||
status DocumentStatus @default(DRAFT)
|
||||
source DocumentSource
|
||||
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
|
||||
documents Document[]
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
shareLinks DocumentShareLink[]
|
||||
auditLogs DocumentAuditLog[]
|
||||
|
||||
// Envelope settings
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
|
||||
// Template specific fields.
|
||||
publicTitle String @default("")
|
||||
publicDescription String @default("")
|
||||
directLink TemplateDirectLink?
|
||||
templateId Int? // Todo: Migrate from templateId -> This @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
||||
|
||||
// Relations
|
||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
title String
|
||||
status DocumentStatus @default(DRAFT)
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
shareLinks DocumentShareLink[]
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId String?
|
||||
|
||||
documentMetaId String @unique
|
||||
documentMeta DocumentMeta @relation(fields: [documentMetaId], references: [id])
|
||||
}
|
||||
|
||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.")
|
||||
title String
|
||||
|
||||
documentDataId String
|
||||
documentMeta DocumentMeta?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
completedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
templateId Int?
|
||||
source DocumentSource
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||
|
||||
auditLogs DocumentAuditLog[]
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId String?
|
||||
envelopeId String
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([documentDataId])
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([folderId])
|
||||
}
|
||||
|
||||
model DocumentAuditLog {
|
||||
id String @id @default(cuid())
|
||||
documentId Int
|
||||
envelopeId String
|
||||
createdAt DateTime @default(now())
|
||||
type String
|
||||
data Json
|
||||
@@ -420,7 +444,7 @@ model DocumentAuditLog {
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
|
||||
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum DocumentDataType {
|
||||
@@ -440,7 +464,6 @@ model DocumentData {
|
||||
data String
|
||||
initialData String
|
||||
document Document?
|
||||
template Template?
|
||||
}
|
||||
|
||||
enum DocumentDistributionMethod {
|
||||
@@ -456,8 +479,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 +493,9 @@ model DocumentMeta {
|
||||
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
|
||||
emailReplyTo String?
|
||||
emailId String?
|
||||
|
||||
envelopeId String?
|
||||
envelope Envelope?
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
@@ -501,8 +525,7 @@ enum RecipientRole {
|
||||
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
|
||||
model Recipient {
|
||||
id Int @id @default(autoincrement())
|
||||
documentId Int?
|
||||
templateId Int?
|
||||
envelopeId String
|
||||
email String @db.VarChar(255)
|
||||
name String @default("") @db.VarChar(255)
|
||||
token String
|
||||
@@ -516,15 +539,12 @@ model Recipient {
|
||||
readStatus ReadStatus @default(NOT_OPENED)
|
||||
signingStatus SigningStatus @default(NOT_SIGNED)
|
||||
sendStatus SendStatus @default(NOT_SENT)
|
||||
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
fields Field[]
|
||||
signatures Signature[]
|
||||
|
||||
@@unique([documentId, email])
|
||||
@@unique([templateId, email])
|
||||
@@index([documentId])
|
||||
@@index([templateId])
|
||||
@@unique([envelopeId, email])
|
||||
@@index([envelopeId])
|
||||
@@index([token])
|
||||
}
|
||||
|
||||
@@ -546,8 +566,7 @@ enum FieldType {
|
||||
model Field {
|
||||
id Int @id @default(autoincrement())
|
||||
secondaryId String @unique @default(cuid())
|
||||
documentId Int?
|
||||
templateId Int?
|
||||
envelopeId String
|
||||
recipientId Int
|
||||
type FieldType
|
||||
page Int /// @zod.number.describe("The page number of the field on the document. Starts from 1.")
|
||||
@@ -557,14 +576,12 @@ model Field {
|
||||
height Decimal @default(-1)
|
||||
customText String
|
||||
inserted Boolean
|
||||
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
||||
signature Signature?
|
||||
fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema)
|
||||
|
||||
@@index([documentId])
|
||||
@@index([templateId])
|
||||
@@index([envelopeId])
|
||||
@@index([recipientId])
|
||||
}
|
||||
|
||||
@@ -586,13 +603,13 @@ model DocumentShareLink {
|
||||
id Int @id @default(autoincrement())
|
||||
email String
|
||||
slug String @unique
|
||||
documentId Int
|
||||
envelopeId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([documentId, email])
|
||||
@@unique([envelopeId, email])
|
||||
}
|
||||
|
||||
enum OrganisationType {
|
||||
@@ -803,8 +820,7 @@ model Team {
|
||||
|
||||
profile TeamProfile?
|
||||
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
envelopes Envelope[]
|
||||
folders Folder[]
|
||||
apiTokens ApiToken[]
|
||||
webhooks Webhook[]
|
||||
@@ -837,84 +853,26 @@ model TeamEmailVerification {
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum TemplateType {
|
||||
// TODO: USE THIS
|
||||
// TODO: USE THIS
|
||||
// TODO: USE THIS
|
||||
// TODO: USE THIS
|
||||
enum TemplateDirectLinkType {
|
||||
PUBLIC
|
||||
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())
|
||||
externalId String?
|
||||
type TemplateType @default(PRIVATE)
|
||||
title String
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
templateMeta TemplateMeta?
|
||||
templateDocumentDataId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
publicTitle String @default("")
|
||||
publicDescription String @default("")
|
||||
|
||||
useLegacyFieldInsertion Boolean @default(false)
|
||||
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
teamId Int
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
||||
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
directLink TemplateDirectLink?
|
||||
documents Document[]
|
||||
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||
folderId String?
|
||||
|
||||
@@unique([templateDocumentDataId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model TemplateDirectLink {
|
||||
id String @id @unique @default(cuid())
|
||||
templateId Int @unique
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @unique @default(cuid())
|
||||
envelopeId String @unique
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
enabled Boolean
|
||||
type TemplateDirectLinkType
|
||||
|
||||
directTemplateRecipientId Int
|
||||
|
||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model SiteSettings {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Legacy Document schema to confirm backwards API compatibility since
|
||||
* we migrated Documents to Envelopes.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
|
||||
import DocumentStatusSchema from '../generated/zod/inputTypeSchemas/DocumentStatusSchema';
|
||||
import DocumentVisibilitySchema from '../generated/zod/inputTypeSchemas/DocumentVisibilitySchema';
|
||||
|
||||
const DocumentSourceSchema = z.enum(['DOCUMENT', 'TEMPLATE', 'TEMPLATE_DIRECT_LINK']);
|
||||
const DocumentTypeSchema = z.enum(['DOCUMENT', 'PUBLIC_TEMPLATE', 'PRIVATE_TEMPLATE']);
|
||||
|
||||
/////////////////////////////////////////
|
||||
// DOCUMENT SCHEMA
|
||||
/////////////////////////////////////////
|
||||
|
||||
export const LegacyDocumentSchema = z.object({
|
||||
type: DocumentTypeSchema,
|
||||
visibility: DocumentVisibilitySchema,
|
||||
status: DocumentStatusSchema,
|
||||
source: DocumentSourceSchema,
|
||||
id: z.number(),
|
||||
qrToken: z
|
||||
.string()
|
||||
.describe('The token for viewing the document using the QR code on the certificate.')
|
||||
.nullable(),
|
||||
externalId: z
|
||||
.string()
|
||||
.describe('A custom external ID you can use to identify the document.')
|
||||
.nullable(),
|
||||
secondaryDocumentId: z.number(),
|
||||
secondaryTemplateId: z.number(),
|
||||
publicTitle: z.string(),
|
||||
publicDescription: z.string(),
|
||||
createdFromDocumentId: z.number().nullable(),
|
||||
userId: z.number().describe('The ID of the user that created this document.'),
|
||||
teamId: z.number(),
|
||||
/**
|
||||
* [DocumentAuthOptions]
|
||||
*/
|
||||
authOptions: ZDocumentAuthOptionsSchema.nullable(),
|
||||
/**
|
||||
* [DocumentFormValues]
|
||||
*/
|
||||
formValues: ZDocumentFormValuesSchema.nullable(),
|
||||
title: z.string(),
|
||||
documentDataId: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
completedAt: z.coerce.date().nullable(),
|
||||
deletedAt: z.coerce.date().nullable(),
|
||||
useLegacyFieldInsertion: z.boolean(),
|
||||
folderId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type Document = z.infer<typeof LegacyDocumentSchema>;
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Legacy Template schema to confirm backwards API compatibility since
|
||||
* we removed the "Template" prisma schema model.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||
|
||||
import { DocumentVisibilitySchema } from '../generated/zod/inputTypeSchemas/DocumentVisibilitySchema';
|
||||
|
||||
export const TemplateTypeSchema = z.enum(['PUBLIC', 'PRIVATE']);
|
||||
|
||||
export type TemplateTypeType = `${z.infer<typeof TemplateTypeSchema>}`;
|
||||
|
||||
export const TemplateSchema = z.object({
|
||||
type: TemplateTypeSchema,
|
||||
visibility: DocumentVisibilitySchema,
|
||||
id: z.number(),
|
||||
externalId: z.string().nullable(),
|
||||
title: z.string(),
|
||||
/**
|
||||
* [DocumentAuthOptions]
|
||||
*/
|
||||
authOptions: ZDocumentAuthOptionsSchema.nullable(),
|
||||
templateDocumentDataId: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
publicTitle: z.string(),
|
||||
publicDescription: z.string(),
|
||||
useLegacyFieldInsertion: z.boolean(),
|
||||
userId: z.number(),
|
||||
teamId: z.number(),
|
||||
folderId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type Template = z.infer<typeof TemplateSchema>;
|
||||
@@ -94,7 +94,7 @@ export const createEmbeddingTemplateRoute = procedure
|
||||
emailSettings: meta.emailSettings,
|
||||
};
|
||||
|
||||
await prisma.templateMeta.upsert({
|
||||
await prisma.documentMeta.upsert({
|
||||
where: {
|
||||
templateId: template.id,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user