mirror of
https://github.com/documenso/documenso.git
synced 2025-11-11 13:02:31 +10:00
Compare commits
1 Commits
fix/templa
...
exp/keyboa
| Author | SHA1 | Date | |
|---|---|---|---|
| 357017314d |
@ -136,5 +136,3 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
|
||||
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
|
||||
NEXT_PRIVATE_LOGGER_FILE_PATH=
|
||||
|
||||
# [[PLAIN SUPPORT]]
|
||||
NEXT_PRIVATE_PLAIN_API_KEY=
|
||||
|
||||
@ -1,159 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AdminUserResetTwoFactorDialogProps = {
|
||||
className?: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const AdminUserResetTwoFactorDialog = ({
|
||||
className,
|
||||
user,
|
||||
}: AdminUserResetTwoFactorDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
const [email, setEmail] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: resetTwoFactor, isPending: isResettingTwoFactor } =
|
||||
trpc.admin.user.resetTwoFactor.useMutation();
|
||||
|
||||
const onResetTwoFactor = async () => {
|
||||
try {
|
||||
await resetTwoFactor({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`2FA Reset`),
|
||||
description: _(msg`The user's two factor authentication has been reset successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await revalidate();
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with(AppErrorCode.NOT_FOUND, () => msg`User not found.`)
|
||||
.with(
|
||||
AppErrorCode.UNAUTHORIZED,
|
||||
() => msg`You are not authorized to reset two factor authentcation for this user.`,
|
||||
)
|
||||
.otherwise(
|
||||
() => msg`An error occurred while resetting two factor authentication for the user.`,
|
||||
);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
|
||||
if (!newOpen) {
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>Reset Two Factor Authentication</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
<Trans>
|
||||
Reset the users two factor authentication. This action is irreversible and will
|
||||
disable two factor authentication for the user.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<Trans>Reset 2FA</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="space-y-4">
|
||||
<DialogTitle>
|
||||
<Trans>Reset Two Factor Authentication</Trans>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="selection:bg-red-100">
|
||||
<Trans>
|
||||
This action is irreversible. Please ensure you have informed the user before
|
||||
proceeding.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
To confirm, please enter the accounts email address <br />({user.email}).
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
|
||||
<Input
|
||||
className="mt-2"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={email !== user.email}
|
||||
onClick={onResetTwoFactor}
|
||||
loading={isResettingTwoFactor}
|
||||
>
|
||||
<Trans>Reset 2FA</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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 } from '@prisma/client';
|
||||
import type { DocumentMeta, Recipient, Signature, TemplateMeta } 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 | null;
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
hidePoweredBy?: boolean;
|
||||
allowWhiteLabelling?: boolean;
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentMeta } from '@prisma/client';
|
||||
import type { DocumentMeta, TemplateMeta } 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,
|
||||
DocumentMeta | TemplateMeta,
|
||||
| '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 } from '@prisma/client';
|
||||
import type { DocumentMeta, TemplateMeta } from '@prisma/client';
|
||||
import {
|
||||
type DocumentData,
|
||||
type Field,
|
||||
@ -50,7 +50,7 @@ export type EmbedSignDocumentClientPageProps = {
|
||||
recipient: RecipientWithFields;
|
||||
fields: Field[];
|
||||
completedFields: DocumentField[];
|
||||
metadata?: DocumentMeta | null;
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
isCompleted?: boolean;
|
||||
hidePoweredBy?: boolean;
|
||||
allowWhitelabelling?: boolean;
|
||||
|
||||
@ -1,138 +0,0 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ZSupportTicketSchema = z.object({
|
||||
subject: z.string().min(3, 'Subject is required'),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||
});
|
||||
|
||||
type TSupportTicket = z.infer<typeof ZSupportTicketSchema>;
|
||||
|
||||
export type SupportTicketFormProps = {
|
||||
organisationId: string;
|
||||
teamId?: string | null;
|
||||
onSuccess?: () => void;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export const SupportTicketForm = ({
|
||||
organisationId,
|
||||
teamId,
|
||||
onSuccess,
|
||||
onClose,
|
||||
}: SupportTicketFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: submitSupportTicket, isPending } =
|
||||
trpc.profile.submitSupportTicket.useMutation();
|
||||
|
||||
const form = useForm<TSupportTicket>({
|
||||
resolver: zodResolver(ZSupportTicketSchema),
|
||||
defaultValues: {
|
||||
subject: '',
|
||||
message: '',
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isLoading || isPending;
|
||||
|
||||
const onSubmit = async (data: TSupportTicket) => {
|
||||
const { subject, message } = data;
|
||||
|
||||
try {
|
||||
await submitSupportTicket({
|
||||
subject,
|
||||
message,
|
||||
organisationId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t`Support ticket created`,
|
||||
description: t`Your support request has been submitted. We'll get back to you soon!`,
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: t`Failed to create support ticket`,
|
||||
description: t`An error occurred. Please try again later.`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<fieldset disabled={isLoading} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>Subject</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>
|
||||
<Trans>Message</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={5} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-2 flex flex-row gap-2">
|
||||
<Button type="submit" size="sm" loading={isLoading}>
|
||||
<Trans>Submit</Trans>
|
||||
</Button>
|
||||
{onClose && (
|
||||
<Button variant="outline" size="sm" type="button" onClick={onClose}>
|
||||
<Trans>Close</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -36,6 +36,7 @@ export type DocumentSigningSignatureFieldProps = {
|
||||
typedSignatureEnabled?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
drawSignatureEnabled?: boolean;
|
||||
keyboardSignatureEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningSignatureField = ({
|
||||
@ -45,6 +46,7 @@ export const DocumentSigningSignatureField = ({
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
keyboardSignatureEnabled,
|
||||
}: DocumentSigningSignatureFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@ -283,6 +285,7 @@ export const DocumentSigningSignatureField = ({
|
||||
typedSignatureEnabled={typedSignatureEnabled}
|
||||
uploadSignatureEnabled={uploadSignatureEnabled}
|
||||
drawSignatureEnabled={drawSignatureEnabled}
|
||||
keyboardSignatureEnabled={keyboardSignatureEnabled}
|
||||
/>
|
||||
|
||||
<DocumentSigningDisclosure />
|
||||
|
||||
@ -321,19 +321,6 @@ export const OrgMenuSwitcher = () => {
|
||||
<Trans>Language</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{currentOrganisation && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/o/${currentOrganisation.url}/support`,
|
||||
search: currentTeam ? `?team=${currentTeam.id}` : '',
|
||||
}}
|
||||
>
|
||||
<Trans>Support</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
|
||||
onSelect={async () => authClient.signOut()}
|
||||
|
||||
@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { Role, Subscription } from '@prisma/client';
|
||||
import type { Document, 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;
|
||||
documentCount: number;
|
||||
documents: DocumentLite[];
|
||||
};
|
||||
|
||||
type SubscriptionLite = Pick<
|
||||
@ -28,6 +28,8 @@ type SubscriptionLite = Pick<
|
||||
'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd'
|
||||
>;
|
||||
|
||||
type DocumentLite = Pick<Document, 'id'>;
|
||||
|
||||
type AdminDashboardUsersTableProps = {
|
||||
users: UserData[];
|
||||
totalPages: number;
|
||||
@ -72,7 +74,10 @@ export const AdminDashboardUsersTable = ({
|
||||
},
|
||||
{
|
||||
header: _(msg`Documents`),
|
||||
accessorKey: 'documentCount',
|
||||
accessorKey: 'documents',
|
||||
cell: ({ row }) => {
|
||||
return <div>{row.original.documents?.length}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
|
||||
@ -27,7 +27,6 @@ import { AdminOrganisationCreateDialog } from '~/components/dialogs/admin-organi
|
||||
import { AdminUserDeleteDialog } from '~/components/dialogs/admin-user-delete-dialog';
|
||||
import { AdminUserDisableDialog } from '~/components/dialogs/admin-user-disable-dialog';
|
||||
import { AdminUserEnableDialog } from '~/components/dialogs/admin-user-enable-dialog';
|
||||
import { AdminUserResetTwoFactorDialog } from '~/components/dialogs/admin-user-reset-two-factor-dialog';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
import { AdminOrganisationsTable } from '~/components/tables/admin-organisations-table';
|
||||
|
||||
@ -220,11 +219,10 @@ const AdminUserPage = ({ user }: { user: User }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 flex flex-col gap-4">
|
||||
{user && user.twoFactorEnabled && <AdminUserResetTwoFactorDialog user={user} />}
|
||||
<div className="mt-16 flex flex-col items-center gap-4">
|
||||
{user && <AdminUserDeleteDialog user={user} />}
|
||||
{user && user.disabled && <AdminUserEnableDialog userToEnable={user} />}
|
||||
{user && !user.disabled && <AdminUserDisableDialog userToDisable={user} />}
|
||||
{user && <AdminUserDeleteDialog user={user} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,125 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { BookIcon, HelpCircleIcon, Link2Icon } from 'lucide-react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { SupportTicketForm } from '~/components/forms/support-ticket-form';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Support');
|
||||
}
|
||||
|
||||
export default function SupportPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const { user } = useSession();
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const teamId = searchParams.get('team');
|
||||
|
||||
const subscriptionStatus = organisation.subscription?.status;
|
||||
|
||||
const handleSuccess = () => {
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleCloseForm = () => {
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="flex flex-row items-center gap-2 text-3xl font-bold">
|
||||
<HelpCircleIcon className="text-muted-foreground h-8 w-8" />
|
||||
<Trans>Support</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-2">
|
||||
<Trans>Your current plan includes the following support channels:</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4">
|
||||
<div className="rounded-lg border p-4">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<BookIcon className="text-muted-foreground h-5 w-5" />
|
||||
<Link
|
||||
to="https://docs.documenso.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
<Trans>Documentation</Trans>
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<Trans>Read our documentation to get started with Documenso.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<Link2Icon className="text-muted-foreground h-5 w-5" />
|
||||
<Link
|
||||
to="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
<Trans>Discord</Trans>
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<Trans>
|
||||
Join our community on{' '}
|
||||
<Link
|
||||
to="https://documen.so/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
Discord
|
||||
</Link>{' '}
|
||||
for community support and discussion.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
{organisation && IS_BILLING_ENABLED() && subscriptionStatus && (
|
||||
<>
|
||||
<div className="rounded-lg border p-4">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<Link2Icon className="text-muted-foreground h-5 w-5" />
|
||||
<Trans>Contact us</Trans>
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<Trans>We'll get back to you as soon as possible via email.</Trans>
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
{!showForm ? (
|
||||
<Button variant="outline" size="sm" onClick={() => setShowForm(true)}>
|
||||
<Trans>Create a support ticket</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<SupportTicketForm
|
||||
organisationId={organisation.id}
|
||||
teamId={teamId}
|
||||
onSuccess={handleSuccess}
|
||||
onClose={handleCloseForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@ -3522,15 +3522,6 @@
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@graphql-typed-document-node/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
||||
@ -11835,20 +11826,6 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@team-plain/typescript-sdk": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-5.9.0.tgz",
|
||||
"integrity": "sha512-AHSXyt1kDt74m9YKZBCRCd6cQjB8QjUNr9cehtR2QHzZ/8yXJPzawPJDqOQ3ms5KvwuYrBx2qT3e6C/zrQ5UtA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"graphql": "^16.6.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"zod": "3.22.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@theguild/remark-mermaid": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@theguild/remark-mermaid/-/remark-mermaid-0.0.5.tgz",
|
||||
@ -13258,6 +13235,7 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@ -13270,23 +13248,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@ -18810,6 +18771,7 @@
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -19885,15 +19847,6 @@
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
|
||||
"integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gray-matter": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
@ -22376,6 +22329,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
@ -30616,6 +30570,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -36628,7 +36583,6 @@
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@team-plain/typescript-sdk": "^5.9.0",
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"inngest": "^3.19.13",
|
||||
|
||||
@ -554,12 +554,6 @@ 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,
|
||||
|
||||
@ -69,4 +69,11 @@ export const DOCUMENT_SIGNATURE_TYPES = {
|
||||
}),
|
||||
value: DocumentSignatureType.UPLOAD,
|
||||
},
|
||||
[DocumentSignatureType.KEYBOARD]: {
|
||||
label: msg({
|
||||
message: `Keyboard`,
|
||||
context: `Keyboard signatute type`,
|
||||
}),
|
||||
value: DocumentSignatureType.KEYBOARD,
|
||||
},
|
||||
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;
|
||||
|
||||
@ -33,7 +33,6 @@
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@team-plain/typescript-sdk": "^5.9.0",
|
||||
"@vvo/tzdb": "^6.117.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"inngest": "^3.19.13",
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import { PlainClient } from '@team-plain/typescript-sdk';
|
||||
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
export const plainClient = new PlainClient({
|
||||
apiKey: env('NEXT_PRIVATE_PLAIN_API_KEY') ?? '',
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentMeta, DocumentVisibility } from '@prisma/client';
|
||||
import type { DocumentVisibility, TemplateMeta } from '@prisma/client';
|
||||
import {
|
||||
DocumentSource,
|
||||
RecipientRole,
|
||||
@ -46,7 +46,7 @@ export type CreateDocumentOptions = {
|
||||
formValues?: TDocumentFormValues;
|
||||
recipients: TCreateDocumentV2Request['recipients'];
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
};
|
||||
|
||||
|
||||
@ -1,32 +1,39 @@
|
||||
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 { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { DocumentVisibility } from '../../types/document-visibility';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type GetDocumentByIdOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId: number;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumentByIdOptions) => {
|
||||
const { documentWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
validatedUserId: userId,
|
||||
unvalidatedTeamId: teamId,
|
||||
export const getDocumentById = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
folderId,
|
||||
}: GetDocumentByIdOptions) => {
|
||||
const { documentWhereInput } = await getDocumentWhereInput({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: documentWhereInput,
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
...documentWhereInput,
|
||||
folderId,
|
||||
},
|
||||
include: {
|
||||
documents: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
@ -49,7 +56,7 @@ export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumen
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document could not be found',
|
||||
});
|
||||
@ -57,3 +64,93 @@ export const getDocumentById = async ({ documentId, userId, teamId }: GetDocumen
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export interface GetDocumentMetaByDocumentIdOptions {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const getDocumentMetaByDocumentId = async ({ id }: GetDocumentMetaByDocumentIdOptions) => {
|
||||
return await prisma.documentMeta.findFirstOrThrow({
|
||||
where: {
|
||||
documentId: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,157 +0,0 @@
|
||||
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 { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
|
||||
import type { DocumentVisibility, Template, TemplateMeta } 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<DocumentMeta, 'id' | 'templateId'>>;
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
};
|
||||
|
||||
export const ZCreateTemplateResponseSchema = TemplateSchema;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
|
||||
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -22,7 +22,7 @@ export type UpdateTemplateOptions = {
|
||||
type?: Template['type'];
|
||||
useLegacyFieldInsertion?: boolean;
|
||||
};
|
||||
meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
|
||||
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
|
||||
};
|
||||
|
||||
export const updateTemplate = async ({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -25,10 +25,9 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => {
|
||||
const serviceAccount = await deletedAccountServiceAccount();
|
||||
|
||||
// TODO: Send out cancellations for all pending docs
|
||||
await prisma.envelope.updateMany({
|
||||
await prisma.document.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: {
|
||||
in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED],
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { EnvelopeType, Prisma } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -34,20 +34,12 @@ export const findUsers = async ({
|
||||
|
||||
const [users, count] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
select: {
|
||||
_count: {
|
||||
include: {
|
||||
documents: {
|
||||
select: {
|
||||
envelopes: {
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
roles: true,
|
||||
},
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
@ -59,10 +51,7 @@ export const findUsers = async ({
|
||||
]);
|
||||
|
||||
return {
|
||||
users: users.map((user) => ({
|
||||
...user,
|
||||
documentCount: user._count.envelopes,
|
||||
})),
|
||||
users,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
import { plainClient } from '@documenso/lib/plain/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
type SubmitSupportTicketOptions = {
|
||||
subject: string;
|
||||
message: string;
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
teamId?: number | null;
|
||||
};
|
||||
|
||||
export const submitSupportTicket = async ({
|
||||
subject,
|
||||
message,
|
||||
userId,
|
||||
organisationId,
|
||||
teamId,
|
||||
}: SubmitSupportTicketOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
const team = teamId
|
||||
? await getTeamById({
|
||||
userId,
|
||||
teamId,
|
||||
})
|
||||
: null;
|
||||
|
||||
const customMessage = `
|
||||
Organisation: ${organisation.name} (${organisation.id})
|
||||
Team: ${team ? `${team.name} (${team.id})` : 'No team provided'}
|
||||
|
||||
${message}`;
|
||||
|
||||
const res = await plainClient.createThread({
|
||||
title: subject,
|
||||
customerIdentifier: { emailAddress: user.email },
|
||||
components: [{ componentText: { text: customMessage } }],
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import type { 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 = LegacyDocumentSchema.pick({
|
||||
export const ZDocumentSchema = DocumentSchema.pick({
|
||||
visibility: true,
|
||||
status: true,
|
||||
source: true,
|
||||
@ -31,12 +31,9 @@ export const ZDocumentSchema = LegacyDocumentSchema.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,
|
||||
@ -85,7 +82,7 @@ export type TDocument = z.infer<typeof ZDocumentSchema>;
|
||||
/**
|
||||
* A lite version of the document response schema without relations.
|
||||
*/
|
||||
export const ZDocumentLiteSchema = LegacyDocumentSchema.pick({
|
||||
export const ZDocumentLiteSchema = DocumentSchema.pick({
|
||||
visibility: true,
|
||||
status: true,
|
||||
source: true,
|
||||
@ -101,12 +98,9 @@ export const ZDocumentLiteSchema = LegacyDocumentSchema.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>;
|
||||
@ -114,7 +108,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 = LegacyDocumentSchema.pick({
|
||||
export const ZDocumentManySchema = DocumentSchema.pick({
|
||||
visibility: true,
|
||||
status: true,
|
||||
source: true,
|
||||
@ -130,13 +124,10 @@ export const ZDocumentManySchema = LegacyDocumentSchema.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,6 +18,8 @@ export const ZFieldSchema = FieldSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
secondaryId: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
recipientId: true,
|
||||
page: true,
|
||||
positionX: true,
|
||||
@ -27,10 +29,6 @@ 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,5 +1,3 @@
|
||||
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';
|
||||
@ -17,6 +15,8 @@ export const ZRecipientSchema = RecipientSchema.pick({
|
||||
signingStatus: true,
|
||||
sendStatus: true,
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
token: true,
|
||||
@ -28,10 +28,6 @@ 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(),
|
||||
});
|
||||
|
||||
/**
|
||||
@ -43,6 +39,8 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({
|
||||
signingStatus: true,
|
||||
sendStatus: true,
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
token: true,
|
||||
@ -52,10 +50,6 @@ 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(),
|
||||
});
|
||||
|
||||
/**
|
||||
@ -67,6 +61,8 @@ export const ZRecipientManySchema = RecipientSchema.pick({
|
||||
signingStatus: true,
|
||||
sendStatus: true,
|
||||
id: true,
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
email: true,
|
||||
name: true,
|
||||
token: true,
|
||||
@ -87,8 +83,4 @@ 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 { z } from 'zod';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
|
||||
import { TemplateMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateMetaSchema';
|
||||
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
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: DocumentMetaSchema.pick({
|
||||
templateMeta: TemplateMetaSchema.pick({
|
||||
id: true,
|
||||
subject: true,
|
||||
message: true,
|
||||
@ -51,17 +51,13 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
drawSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
distributionMethod: true,
|
||||
templateId: true,
|
||||
redirectUrl: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
emailId: true,
|
||||
emailReplyTo: true,
|
||||
})
|
||||
.extend({
|
||||
// Legacy field for backwards compatibility. Needs to refer to the Envelope `secondaryTemplateId`.
|
||||
templateId: z.number(),
|
||||
})
|
||||
.nullable(),
|
||||
}).nullable(),
|
||||
directLink: TemplateDirectLinkSchema.nullable(),
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
@ -133,7 +129,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({
|
||||
}).nullable(),
|
||||
fields: ZFieldSchema.array(),
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
templateMeta: DocumentMetaSchema.pick({
|
||||
templateMeta: TemplateMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
}).nullable(),
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import type { DocumentMeta, Envelope, OrganisationGlobalSettings } from '@prisma/client';
|
||||
import type {
|
||||
Document,
|
||||
DocumentMeta,
|
||||
OrganisationGlobalSettings,
|
||||
TemplateMeta,
|
||||
} 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<Envelope, 'status'> | DocumentStatus) => {
|
||||
export const isDocumentCompleted = (document: Pick<Document, 'status'> | DocumentStatus) => {
|
||||
const status = typeof document === 'string' ? document : document.status;
|
||||
|
||||
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
|
||||
@ -24,7 +29,7 @@ export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | Documen
|
||||
*/
|
||||
export const extractDerivedDocumentMeta = (
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>,
|
||||
overrideMeta: Partial<DocumentMeta> | undefined | null,
|
||||
overrideMeta: Partial<DocumentMeta | TemplateMeta> | undefined | null,
|
||||
) => {
|
||||
const meta = overrideMeta ?? {};
|
||||
|
||||
@ -53,5 +58,5 @@ export const extractDerivedDocumentMeta = (
|
||||
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
|
||||
emailSettings:
|
||||
meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
|
||||
} satisfies Omit<DocumentMeta, 'id' | 'envelopeId'>;
|
||||
} satisfies Omit<DocumentMeta, 'id' | 'documentId'>;
|
||||
};
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@ -18,6 +18,7 @@ export enum DocumentSignatureType {
|
||||
DRAW = 'draw',
|
||||
TYPE = 'type',
|
||||
UPLOAD = 'upload',
|
||||
KEYBOARD = 'keyboard',
|
||||
}
|
||||
|
||||
export const formatTeamUrl = (teamUrl: string, baseUrl?: string) => {
|
||||
@ -83,10 +84,16 @@ export const extractTeamSignatureSettings = (
|
||||
typedSignatureEnabled: boolean | null;
|
||||
drawSignatureEnabled: boolean | null;
|
||||
uploadSignatureEnabled: boolean | null;
|
||||
keyboardSignatureEnabled?: boolean | null;
|
||||
} | null,
|
||||
) => {
|
||||
if (!settings) {
|
||||
return [DocumentSignatureType.TYPE, DocumentSignatureType.UPLOAD, DocumentSignatureType.DRAW];
|
||||
return [
|
||||
DocumentSignatureType.TYPE,
|
||||
DocumentSignatureType.UPLOAD,
|
||||
DocumentSignatureType.DRAW,
|
||||
DocumentSignatureType.KEYBOARD,
|
||||
];
|
||||
}
|
||||
|
||||
const signatureTypes: DocumentSignatureType[] = [];
|
||||
@ -103,6 +110,10 @@ export const extractTeamSignatureSettings = (
|
||||
signatureTypes.push(DocumentSignatureType.UPLOAD);
|
||||
}
|
||||
|
||||
if (settings.keyboardSignatureEnabled !== false) {
|
||||
signatureTypes.push(DocumentSignatureType.KEYBOARD);
|
||||
}
|
||||
|
||||
return signatureTypes;
|
||||
};
|
||||
|
||||
@ -175,6 +186,7 @@ export const generateDefaultTeamSettings = (): Omit<TeamGlobalSettings, 'id' | '
|
||||
typedSignatureEnabled: null,
|
||||
uploadSignatureEnabled: null,
|
||||
drawSignatureEnabled: null,
|
||||
keyboardSignatureEnabled: null,
|
||||
|
||||
brandingEnabled: null,
|
||||
brandingLogo: null,
|
||||
|
||||
@ -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,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "keyboardSignatureEnabled" BOOLEAN;
|
||||
@ -1,57 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,261 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
@ -1,11 +0,0 @@
|
||||
/*
|
||||
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;
|
||||
@ -64,7 +64,8 @@ model User {
|
||||
twoFactorBackupCodes String?
|
||||
|
||||
folders Folder[]
|
||||
envelopes Envelope[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
|
||||
verificationTokens VerificationToken[]
|
||||
apiTokens ApiToken[]
|
||||
@ -347,7 +348,8 @@ model Folder {
|
||||
pinned Boolean @default(false)
|
||||
parentId String?
|
||||
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
envelopes Envelope[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
subfolders Folder[] @relation("FolderToFolder")
|
||||
@ -360,79 +362,53 @@ model Folder {
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
enum EnvelopeType {
|
||||
DOCUMENT
|
||||
TEMPLATE
|
||||
}
|
||||
/// @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.")
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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[]
|
||||
documentDataId String
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
documentMeta DocumentMeta?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
completedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
templateId Int?
|
||||
source DocumentSource
|
||||
|
||||
envelopeId String
|
||||
envelope Envelope @relation(fields: [envelopeId], 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?
|
||||
|
||||
@@unique([documentDataId])
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([folderId])
|
||||
}
|
||||
|
||||
model DocumentAuditLog {
|
||||
id String @id @default(cuid())
|
||||
envelopeId String
|
||||
documentId Int
|
||||
createdAt DateTime @default(now())
|
||||
type String
|
||||
data Json
|
||||
@ -444,7 +420,7 @@ model DocumentAuditLog {
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum DocumentDataType {
|
||||
@ -464,6 +440,7 @@ model DocumentData {
|
||||
data String
|
||||
initialData String
|
||||
document Document?
|
||||
template Template?
|
||||
}
|
||||
|
||||
enum DocumentDistributionMethod {
|
||||
@ -479,6 +456,8 @@ 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)
|
||||
@ -493,9 +472,6 @@ model DocumentMeta {
|
||||
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
|
||||
emailReplyTo String?
|
||||
emailId String?
|
||||
|
||||
envelopeId String?
|
||||
envelope Envelope?
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
@ -525,7 +501,8 @@ enum RecipientRole {
|
||||
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
|
||||
model Recipient {
|
||||
id Int @id @default(autoincrement())
|
||||
envelopeId String
|
||||
documentId Int?
|
||||
templateId Int?
|
||||
email String @db.VarChar(255)
|
||||
name String @default("") @db.VarChar(255)
|
||||
token String
|
||||
@ -539,12 +516,15 @@ model Recipient {
|
||||
readStatus ReadStatus @default(NOT_OPENED)
|
||||
signingStatus SigningStatus @default(NOT_SIGNED)
|
||||
sendStatus SendStatus @default(NOT_SENT)
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
fields Field[]
|
||||
signatures Signature[]
|
||||
|
||||
@@unique([envelopeId, email])
|
||||
@@index([envelopeId])
|
||||
@@unique([documentId, email])
|
||||
@@unique([templateId, email])
|
||||
@@index([documentId])
|
||||
@@index([templateId])
|
||||
@@index([token])
|
||||
}
|
||||
|
||||
@ -566,7 +546,8 @@ enum FieldType {
|
||||
model Field {
|
||||
id Int @id @default(autoincrement())
|
||||
secondaryId String @unique @default(cuid())
|
||||
envelopeId String
|
||||
documentId Int?
|
||||
templateId Int?
|
||||
recipientId Int
|
||||
type FieldType
|
||||
page Int /// @zod.number.describe("The page number of the field on the document. Starts from 1.")
|
||||
@ -576,12 +557,14 @@ model Field {
|
||||
height Decimal @default(-1)
|
||||
customText String
|
||||
inserted Boolean
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
||||
signature Signature?
|
||||
fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema)
|
||||
|
||||
@@index([envelopeId])
|
||||
@@index([documentId])
|
||||
@@index([templateId])
|
||||
@@index([recipientId])
|
||||
}
|
||||
|
||||
@ -603,13 +586,13 @@ model DocumentShareLink {
|
||||
id Int @id @default(autoincrement())
|
||||
email String
|
||||
slug String @unique
|
||||
envelopeId String
|
||||
documentId Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([envelopeId, email])
|
||||
@@unique([documentId, email])
|
||||
}
|
||||
|
||||
enum OrganisationType {
|
||||
@ -793,6 +776,7 @@ model TeamGlobalSettings {
|
||||
typedSignatureEnabled Boolean?
|
||||
uploadSignatureEnabled Boolean?
|
||||
drawSignatureEnabled Boolean?
|
||||
keyboardSignatureEnabled Boolean?
|
||||
|
||||
emailId String?
|
||||
email OrganisationEmail? @relation(fields: [emailId], references: [id])
|
||||
@ -820,7 +804,8 @@ model Team {
|
||||
|
||||
profile TeamProfile?
|
||||
|
||||
envelopes Envelope[]
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
folders Folder[]
|
||||
apiTokens ApiToken[]
|
||||
webhooks Webhook[]
|
||||
@ -853,26 +838,84 @@ model TeamEmailVerification {
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
// TODO: USE THIS
|
||||
// TODO: USE THIS
|
||||
// TODO: USE THIS
|
||||
// TODO: USE THIS
|
||||
enum TemplateDirectLinkType {
|
||||
enum TemplateType {
|
||||
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())
|
||||
envelopeId String @unique
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @unique @default(cuid())
|
||||
templateId Int @unique
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
enabled Boolean
|
||||
type TemplateDirectLinkType
|
||||
|
||||
directTemplateRecipientId Int
|
||||
|
||||
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
|
||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model SiteSettings {
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* 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>;
|
||||
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 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>;
|
||||
@ -1,50 +0,0 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZResetTwoFactorRequestSchema,
|
||||
ZResetTwoFactorResponseSchema,
|
||||
} from './reset-two-factor-authentication.types';
|
||||
|
||||
export const resetTwoFactorRoute = adminProcedure
|
||||
.input(ZResetTwoFactorRequestSchema)
|
||||
.output(ZResetTwoFactorResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { userId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return await resetTwoFactor({ userId });
|
||||
});
|
||||
|
||||
export type ResetTwoFactorOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const resetTwoFactor = async ({ userId }: ResetTwoFactorOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: false,
|
||||
twoFactorBackupCodes: null,
|
||||
twoFactorSecret: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZResetTwoFactorRequestSchema = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export const ZResetTwoFactorResponseSchema = z.void();
|
||||
|
||||
export type TResetTwoFactorRequest = z.infer<typeof ZResetTwoFactorRequestSchema>;
|
||||
export type TResetTwoFactorResponse = z.infer<typeof ZResetTwoFactorResponseSchema>;
|
||||
@ -21,7 +21,6 @@ import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
|
||||
import { findAdminOrganisationsRoute } from './find-admin-organisations';
|
||||
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
|
||||
import { getAdminOrganisationRoute } from './get-admin-organisation';
|
||||
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
|
||||
import {
|
||||
ZAdminDeleteDocumentMutationSchema,
|
||||
ZAdminDeleteUserMutationSchema,
|
||||
@ -52,9 +51,6 @@ export const adminRouter = router({
|
||||
stripe: {
|
||||
createCustomer: createStripeCustomerRoute,
|
||||
},
|
||||
user: {
|
||||
resetTwoFactor: resetTwoFactorRoute,
|
||||
},
|
||||
|
||||
// Todo: migrate old routes
|
||||
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
||||
|
||||
@ -94,7 +94,7 @@ export const createEmbeddingTemplateRoute = procedure
|
||||
emailSettings: meta.emailSettings,
|
||||
};
|
||||
|
||||
await prisma.documentMeta.upsert({
|
||||
await prisma.templateMeta.upsert({
|
||||
where: {
|
||||
templateId: template.id,
|
||||
},
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
|
||||
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
|
||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
import { submitSupportTicket } from '@documenso/lib/server-only/user/submit-support-ticket';
|
||||
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
||||
|
||||
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
|
||||
@ -12,7 +10,6 @@ import {
|
||||
ZFindUserSecurityAuditLogsSchema,
|
||||
ZRetrieveUserByIdQuerySchema,
|
||||
ZSetProfileImageMutationSchema,
|
||||
ZSubmitSupportTicketMutationSchema,
|
||||
ZUpdateProfileMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
@ -94,28 +91,4 @@ export const profileRouter = router({
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}),
|
||||
|
||||
submitSupportTicket: authenticatedProcedure
|
||||
.input(ZSubmitSupportTicketMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { subject, message, organisationId, teamId } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
const parsedTeamId = teamId ? Number(teamId) : null;
|
||||
|
||||
if (Number.isNaN(parsedTeamId)) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid team ID provided',
|
||||
});
|
||||
}
|
||||
|
||||
return await submitSupportTicket({
|
||||
subject,
|
||||
message,
|
||||
userId,
|
||||
organisationId,
|
||||
teamId: parsedTeamId,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@ -27,12 +27,3 @@ export const ZSetProfileImageMutationSchema = z.object({
|
||||
});
|
||||
|
||||
export type TSetProfileImageMutationSchema = z.infer<typeof ZSetProfileImageMutationSchema>;
|
||||
|
||||
export const ZSubmitSupportTicketMutationSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
teamId: z.string().min(1).nullish(),
|
||||
subject: z.string().min(3, 'Subject is required'),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters'),
|
||||
});
|
||||
|
||||
export type TSupportTicketRequest = z.infer<typeof ZSubmitSupportTicketMutationSchema>;
|
||||
|
||||
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||
import type { DocumentMeta, Field, Recipient, TemplateMeta } 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, 'dateFormat'>;
|
||||
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
|
||||
|
||||
showFieldStatus?: boolean;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { DocumentMeta, Signature } from '@prisma/client';
|
||||
import type { DocumentMeta, Signature, TemplateMeta } 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, 'dateFormat'>;
|
||||
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
207
packages/ui/primitives/signature-pad/keyboard-utils.ts
Normal file
207
packages/ui/primitives/signature-pad/keyboard-utils.ts
Normal file
@ -0,0 +1,207 @@
|
||||
export enum KeyboardLayout {
|
||||
QWERTY = 'QWERTY',
|
||||
}
|
||||
|
||||
export type CurveType =
|
||||
| 'linear'
|
||||
| 'simple-curve'
|
||||
| 'quadratic-bezier'
|
||||
| 'cubic-bezier'
|
||||
| 'catmull-rom';
|
||||
|
||||
export enum StrokeStyle {
|
||||
SOLID = 'solid',
|
||||
GRADIENT = 'gradient',
|
||||
}
|
||||
|
||||
export interface StrokeConfig {
|
||||
style: StrokeStyle;
|
||||
color: string;
|
||||
gradientStart: string;
|
||||
gradientEnd: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export const getKeyboardLayout = (
|
||||
layout: KeyboardLayout,
|
||||
includeNumbers: boolean = false,
|
||||
): Record<string, Point> => {
|
||||
const qwertyLayout: Record<string, Point> = {
|
||||
Q: { x: 0, y: includeNumbers ? 1 : 0 },
|
||||
W: { x: 1, y: includeNumbers ? 1 : 0 },
|
||||
E: { x: 2, y: includeNumbers ? 1 : 0 },
|
||||
R: { x: 3, y: includeNumbers ? 1 : 0 },
|
||||
T: { x: 4, y: includeNumbers ? 1 : 0 },
|
||||
Y: { x: 5, y: includeNumbers ? 1 : 0 },
|
||||
U: { x: 6, y: includeNumbers ? 1 : 0 },
|
||||
I: { x: 7, y: includeNumbers ? 1 : 0 },
|
||||
O: { x: 8, y: includeNumbers ? 1 : 0 },
|
||||
P: { x: 9, y: includeNumbers ? 1 : 0 },
|
||||
|
||||
A: { x: 0.5, y: includeNumbers ? 2 : 1 },
|
||||
S: { x: 1.5, y: includeNumbers ? 2 : 1 },
|
||||
D: { x: 2.5, y: includeNumbers ? 2 : 1 },
|
||||
F: { x: 3.5, y: includeNumbers ? 2 : 1 },
|
||||
G: { x: 4.5, y: includeNumbers ? 2 : 1 },
|
||||
H: { x: 5.5, y: includeNumbers ? 2 : 1 },
|
||||
J: { x: 6.5, y: includeNumbers ? 2 : 1 },
|
||||
K: { x: 7.5, y: includeNumbers ? 2 : 1 },
|
||||
L: { x: 8.5, y: includeNumbers ? 2 : 1 },
|
||||
|
||||
Z: { x: 1, y: includeNumbers ? 3 : 2 },
|
||||
X: { x: 2, y: includeNumbers ? 3 : 2 },
|
||||
C: { x: 3, y: includeNumbers ? 3 : 2 },
|
||||
V: { x: 4, y: includeNumbers ? 3 : 2 },
|
||||
B: { x: 5, y: includeNumbers ? 3 : 2 },
|
||||
N: { x: 6, y: includeNumbers ? 3 : 2 },
|
||||
M: { x: 7, y: includeNumbers ? 3 : 2 },
|
||||
};
|
||||
|
||||
if (includeNumbers) {
|
||||
const numberRow = {
|
||||
'1': { x: 0, y: 0 },
|
||||
'2': { x: 1, y: 0 },
|
||||
'3': { x: 2, y: 0 },
|
||||
'4': { x: 3, y: 0 },
|
||||
'5': { x: 4, y: 0 },
|
||||
'6': { x: 5, y: 0 },
|
||||
'7': { x: 6, y: 0 },
|
||||
'8': { x: 7, y: 0 },
|
||||
'9': { x: 8, y: 0 },
|
||||
'0': { x: 9, y: 0 },
|
||||
};
|
||||
return { ...numberRow, ...qwertyLayout };
|
||||
}
|
||||
|
||||
return qwertyLayout;
|
||||
};
|
||||
|
||||
export const generatePath = (points: Point[], curveType: CurveType): string => {
|
||||
if (points.length === 0) return '';
|
||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
switch (curveType) {
|
||||
case 'linear':
|
||||
return generateLinearPath(points);
|
||||
case 'simple-curve':
|
||||
return generateSimpleCurvePath(points);
|
||||
case 'quadratic-bezier':
|
||||
return generateQuadraticBezierPath(points);
|
||||
case 'cubic-bezier':
|
||||
return generateCubicBezierPath(points);
|
||||
case 'catmull-rom':
|
||||
return generateCatmullRomPath(points);
|
||||
default:
|
||||
return generateLinearPath(points);
|
||||
}
|
||||
};
|
||||
|
||||
const generateLinearPath = (points: Point[]): string => {
|
||||
if (points.length === 0) return '';
|
||||
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
path += ` L ${points[i].x} ${points[i].y}`;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const generateSimpleCurvePath = (points: Point[]): string => {
|
||||
if (points.length === 0) return '';
|
||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
|
||||
const midX = (prev.x + curr.x) / 2;
|
||||
const midY = (prev.y + curr.y) / 2;
|
||||
|
||||
if (i === 1) {
|
||||
path += ` Q ${prev.x} ${prev.y} ${midX} ${midY}`;
|
||||
} else {
|
||||
path += ` T ${midX} ${midY}`;
|
||||
}
|
||||
}
|
||||
|
||||
const lastPoint = points[points.length - 1];
|
||||
path += ` T ${lastPoint.x} ${lastPoint.y}`;
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
const generateQuadraticBezierPath = (points: Point[]): string => {
|
||||
if (points.length === 0) return '';
|
||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
|
||||
const controlX = prev.x + (curr.x - prev.x) * 0.5;
|
||||
const controlY = prev.y - Math.abs(curr.x - prev.x) * 0.3;
|
||||
|
||||
path += ` Q ${controlX} ${controlY} ${curr.x} ${curr.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
const generateCubicBezierPath = (points: Point[]): string => {
|
||||
if (points.length === 0) return '';
|
||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
|
||||
const dx = curr.x - prev.x;
|
||||
const dy = curr.y - prev.y;
|
||||
|
||||
const cp1x = prev.x + dx * 0.25;
|
||||
const cp1y = prev.y + dy * 0.25 - Math.abs(dx) * 0.2;
|
||||
|
||||
const cp2x = prev.x + dx * 0.75;
|
||||
const cp2y = prev.y + dy * 0.75 - Math.abs(dx) * 0.2;
|
||||
|
||||
path += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${curr.x} ${curr.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
const generateCatmullRomPath = (points: Point[]): string => {
|
||||
if (points.length === 0) return '';
|
||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||
if (points.length === 2) return generateLinearPath(points);
|
||||
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const p0 = i === 1 ? points[0] : points[i - 2];
|
||||
const p1 = points[i - 1];
|
||||
const p2 = points[i];
|
||||
const p3 = i === points.length - 1 ? points[i] : points[i + 1];
|
||||
|
||||
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
||||
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
||||
|
||||
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
||||
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
||||
|
||||
path += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
146
packages/ui/primitives/signature-pad/signature-pad-keyboard.tsx
Normal file
146
packages/ui/primitives/signature-pad/signature-pad-keyboard.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { KeyboardLayout, StrokeStyle, generatePath, getKeyboardLayout } from './keyboard-utils';
|
||||
|
||||
export type SignaturePadKeyboardProps = {
|
||||
className?: string;
|
||||
onChange: (_value: string) => void;
|
||||
};
|
||||
|
||||
export const SignaturePadKeyboard = ({ className, onChange }: SignaturePadKeyboardProps) => {
|
||||
const [name, setName] = useState('');
|
||||
const [currentKeyboardLayout] = useState<KeyboardLayout>(KeyboardLayout.QWERTY);
|
||||
|
||||
const curveType = 'linear';
|
||||
const includeNumbers = false;
|
||||
const strokeConfig = {
|
||||
style: StrokeStyle.SOLID,
|
||||
color: '#000000',
|
||||
gradientStart: '#ff6b6b',
|
||||
gradientEnd: '#4ecdc4',
|
||||
width: 3,
|
||||
};
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isInputFocused = document.activeElement === inputRef.current;
|
||||
const isAnyInputFocused = document.activeElement?.tagName === 'INPUT';
|
||||
|
||||
if (!isInputFocused && !isAnyInputFocused) {
|
||||
const regex = includeNumbers ? /^[a-zA-Z0-9]$/ : /^[a-zA-Z]$/;
|
||||
if (regex.test(e.key) || e.key === 'Backspace') {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [includeNumbers]);
|
||||
|
||||
// Generate signature path
|
||||
const signaturePath = useMemo(() => {
|
||||
if (!name) return '';
|
||||
|
||||
const points = [];
|
||||
const currentLayout = getKeyboardLayout(currentKeyboardLayout, includeNumbers);
|
||||
|
||||
for (const char of name.toUpperCase()) {
|
||||
if (char in currentLayout) {
|
||||
const { x, y } = currentLayout[char];
|
||||
const yOffset = includeNumbers ? 100 : 40;
|
||||
points.push({ x: x * 60 + 28, y: y * 60 + yOffset });
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length === 0) return '';
|
||||
return generatePath(points, curveType);
|
||||
}, [name, currentKeyboardLayout, curveType, includeNumbers]);
|
||||
|
||||
// Update parent component when signature changes
|
||||
useEffect(() => {
|
||||
if (signaturePath && name) {
|
||||
// Convert SVG to data URL for consistency with other signature types
|
||||
const svgData = generateSVGDataURL(signaturePath);
|
||||
onChange(svgData);
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
}, [signaturePath, name, onChange]);
|
||||
|
||||
const generateSVGDataURL = (path: string): string => {
|
||||
const height = includeNumbers ? 260 : 200;
|
||||
const gradients =
|
||||
strokeConfig.style === StrokeStyle.GRADIENT
|
||||
? `<linearGradient id="pathGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:${strokeConfig.gradientStart};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${strokeConfig.gradientEnd};stop-opacity:1" />
|
||||
</linearGradient>`
|
||||
: '';
|
||||
const strokeColor =
|
||||
strokeConfig.style === StrokeStyle.SOLID ? strokeConfig.color : 'url(#pathGradient)';
|
||||
|
||||
const svgContent = `<svg width="650" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>${gradients}</defs>
|
||||
<path d="${path}" stroke="${strokeColor}" stroke-width="${strokeConfig.width}" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
|
||||
return `data:image/svg+xml;base64,${btoa(svgContent)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full w-full flex-col items-center justify-center', className)}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="sr-only"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="relative w-full max-w-lg">
|
||||
<svg
|
||||
className="pointer-events-none w-full"
|
||||
viewBox="0 0 650 200"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{ height: '150px' }}
|
||||
>
|
||||
<defs>
|
||||
{strokeConfig.style === StrokeStyle.GRADIENT && (
|
||||
<linearGradient id="pathGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor={strokeConfig.gradientStart} stopOpacity={1} />
|
||||
<stop offset="100%" stopColor={strokeConfig.gradientEnd} stopOpacity={1} />
|
||||
</linearGradient>
|
||||
)}
|
||||
</defs>
|
||||
|
||||
{signaturePath && (
|
||||
<path
|
||||
d={signaturePath}
|
||||
stroke={
|
||||
strokeConfig.style === StrokeStyle.SOLID ? strokeConfig.color : 'url(#pathGradient)'
|
||||
}
|
||||
strokeWidth={strokeConfig.width}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 w-full max-w-lg">
|
||||
<div className="text-muted-foreground/70 font-mono text-xs">{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { KeyboardIcon, UploadCloudIcon } from 'lucide-react';
|
||||
import { Keyboard, KeyboardIcon, UploadCloudIcon } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
@ -10,6 +10,7 @@ import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||
import { SignatureIcon } from '../../icons/signature';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { SignaturePadDraw } from './signature-pad-draw';
|
||||
import { SignaturePadKeyboard } from './signature-pad-keyboard';
|
||||
import { SignaturePadType } from './signature-pad-type';
|
||||
import { SignaturePadUpload } from './signature-pad-upload';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './signature-tabs';
|
||||
@ -28,6 +29,7 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
|
||||
typedSignatureEnabled?: boolean;
|
||||
uploadSignatureEnabled?: boolean;
|
||||
drawSignatureEnabled?: boolean;
|
||||
keyboardSignatureEnabled?: boolean;
|
||||
|
||||
onValidityChange?: (isValid: boolean) => void;
|
||||
};
|
||||
@ -39,10 +41,12 @@ export const SignaturePad = ({
|
||||
typedSignatureEnabled = true,
|
||||
uploadSignatureEnabled = true,
|
||||
drawSignatureEnabled = true,
|
||||
keyboardSignatureEnabled = true,
|
||||
}: SignaturePadProps) => {
|
||||
const [imageSignature, setImageSignature] = useState(isBase64Image(value) ? value : '');
|
||||
const [drawSignature, setDrawSignature] = useState(isBase64Image(value) ? value : '');
|
||||
const [typedSignature, setTypedSignature] = useState(isBase64Image(value) ? '' : value);
|
||||
const [keyboardSignature, setKeyboardSignature] = useState(isBase64Image(value) ? value : '');
|
||||
|
||||
/**
|
||||
* This is cooked.
|
||||
@ -51,7 +55,7 @@ export const SignaturePad = ({
|
||||
* the first enabled tab.
|
||||
*/
|
||||
const [tab, setTab] = useState(
|
||||
((): 'draw' | 'text' | 'image' => {
|
||||
((): 'draw' | 'text' | 'image' | 'keyboard' => {
|
||||
// First passthrough to check to see if there's a signature for a given tab.
|
||||
if (drawSignatureEnabled && drawSignature) {
|
||||
return 'draw';
|
||||
@ -65,6 +69,10 @@ export const SignaturePad = ({
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (keyboardSignatureEnabled && keyboardSignature) {
|
||||
return 'keyboard';
|
||||
}
|
||||
|
||||
// Second passthrough to just select the first avaliable tab.
|
||||
if (drawSignatureEnabled) {
|
||||
return 'draw';
|
||||
@ -78,6 +86,10 @@ export const SignaturePad = ({
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (keyboardSignatureEnabled) {
|
||||
return 'keyboard';
|
||||
}
|
||||
|
||||
throw new Error('No signature enabled');
|
||||
})(),
|
||||
);
|
||||
@ -109,7 +121,16 @@ export const SignaturePad = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onTabChange = (value: 'draw' | 'text' | 'image') => {
|
||||
const onKeyboardSignatureChange = (value: string) => {
|
||||
setKeyboardSignature(value);
|
||||
|
||||
onChange?.({
|
||||
type: DocumentSignatureType.KEYBOARD,
|
||||
value,
|
||||
});
|
||||
};
|
||||
|
||||
const onTabChange = (value: 'draw' | 'text' | 'image' | 'keyboard') => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
@ -126,10 +147,18 @@ export const SignaturePad = ({
|
||||
.with('image', () => {
|
||||
onImageSignatureChange(imageSignature);
|
||||
})
|
||||
.with('keyboard', () => {
|
||||
onKeyboardSignatureChange(keyboardSignature);
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
if (!drawSignatureEnabled && !typedSignatureEnabled && !uploadSignatureEnabled) {
|
||||
if (
|
||||
!drawSignatureEnabled &&
|
||||
!typedSignatureEnabled &&
|
||||
!uploadSignatureEnabled &&
|
||||
!keyboardSignatureEnabled
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -140,7 +169,7 @@ export const SignaturePad = ({
|
||||
'pointer-events-none': disabled,
|
||||
})}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image')}
|
||||
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image' | 'keyboard')}
|
||||
>
|
||||
<TabsList>
|
||||
{drawSignatureEnabled && (
|
||||
@ -163,6 +192,13 @@ export const SignaturePad = ({
|
||||
Upload
|
||||
</TabsTrigger>
|
||||
)}
|
||||
|
||||
{keyboardSignatureEnabled && (
|
||||
<TabsTrigger value="keyboard">
|
||||
<Keyboard className="mr-2 size-4" />
|
||||
Keyboard
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
@ -194,6 +230,13 @@ export const SignaturePad = ({
|
||||
>
|
||||
<SignaturePadUpload value={imageSignature} onChange={onImageSignatureChange} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="keyboard"
|
||||
className="border-border aspect-signature-pad dark:bg-background relative flex items-center justify-center rounded-md border bg-neutral-50 text-center"
|
||||
>
|
||||
<SignaturePadKeyboard value={keyboardSignature} onChange={onKeyboardSignatureChange} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@ -47,7 +47,6 @@
|
||||
"NEXT_PUBLIC_POSTHOG_KEY",
|
||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||
"NEXT_PUBLIC_DISABLE_SIGNUP",
|
||||
"NEXT_PRIVATE_PLAIN_API_KEY",
|
||||
"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT",
|
||||
"NEXT_PRIVATE_DATABASE_URL",
|
||||
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
||||
|
||||
Reference in New Issue
Block a user