mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
8 Commits
feat/docum
...
v1.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b9ae277041 | |||
| 7fad826d06 | |||
| eb8ba2036a | |||
| 339759166c | |||
| 637e06f9c0 | |||
| 332e0657e0 | |||
| 4017b250fb | |||
| 41373a7c6f |
@ -3,6 +3,8 @@ title: Public API
|
||||
description: Learn how to interact with your documents programmatically using the Documenso public API.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Public API
|
||||
|
||||
Documenso provides a public REST API enabling you to interact with your documents programmatically. The API exposes various HTTP endpoints that allow you to perform operations such as:
|
||||
@ -13,10 +15,24 @@ Documenso provides a public REST API enabling you to interact with your document
|
||||
|
||||
The documentation walks you through creating API keys and using them to authenticate your API requests. You'll also learn about the available endpoints, request and response formats, and how to use the API.
|
||||
|
||||
## Swagger Documentation
|
||||
## API V1 - Stable
|
||||
|
||||
The [Swagger documentation](https://app.documenso.com/api/v1/openapi) also provides information about the API endpoints, request parameters, response formats, and authentication methods.
|
||||
Check out the [API V1 documentation](https://app.documenso.com/api/v1/openapi) for details about the API endpoints, request parameters, response formats, and authentication methods.
|
||||
|
||||
## API V2 - Beta
|
||||
|
||||
Our new API V2 is currently in Beta. The new API features typed SDKs for TypeScript, Python and Go and example code for many more.
|
||||
|
||||
<Callout type="warning">
|
||||
NOW IN BETA: [API V2 Documentation](https://documen.so/api-v2-docs)
|
||||
</Callout>
|
||||
|
||||
🚀 [V2 Announcement](https://documen.so/sdk-blog)
|
||||
|
||||
💬 [Leave Feedback](https://documen.so/sdk-feedback)
|
||||
|
||||
🔔 [Breaking Changes](https://documen.so/sdk-breaking)
|
||||
|
||||
## Availability
|
||||
|
||||
The API is available to individual users and teams.
|
||||
The API is available to individual users, teams and higher plans. [Fair Use](https://documen.so/fair) applies.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.11",
|
||||
"version": "1.9.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@ -12,13 +12,14 @@ import {
|
||||
MailOpenIcon,
|
||||
PenIcon,
|
||||
PlusIcon,
|
||||
UserIcon,
|
||||
} from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@ -120,6 +121,12 @@ export const DocumentPageViewRecipients = ({
|
||||
<Trans>Viewed</Trans>
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.ASSISTANT, () => (
|
||||
<>
|
||||
<UserIcon className="mr-1 h-3 w-3" />
|
||||
<Trans>Assisted</Trans>
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@ -162,7 +162,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* We don't want to allow teams moving documents across at the moment. */}
|
||||
{!team && (
|
||||
{!team && !row.teamId && (
|
||||
<DropdownMenuItem onClick={() => setMoveDialogOpen(true)}>
|
||||
<MoveRight className="mr-2 h-4 w-4" />
|
||||
<Trans>Move to Team</Trans>
|
||||
|
||||
@ -40,7 +40,7 @@ export const MoveDocumentDialog = ({ documentId, open, onOpenChange }: MoveDocum
|
||||
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||
|
||||
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
||||
onSuccess: () => {
|
||||
|
||||
@ -81,7 +81,7 @@ export const DataTableActionDropdown = ({
|
||||
<Trans>Direct link</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{!teamId && (
|
||||
{!teamId && !row.teamId && (
|
||||
<DropdownMenuItem onClick={() => setMoveDialogOpen(true)}>
|
||||
<MoveRight className="mr-2 h-4 w-4" />
|
||||
<Trans>Move to Team</Trans>
|
||||
|
||||
@ -42,7 +42,7 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl
|
||||
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||
const { mutateAsync: moveTemplate, isPending } = trpc.template.moveTemplateToTeam.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
|
||||
@ -77,7 +77,11 @@ export const TemplateDirectLinkDialog = ({
|
||||
);
|
||||
|
||||
const validDirectTemplateRecipients = useMemo(
|
||||
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
|
||||
() =>
|
||||
template.recipients.filter(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||
),
|
||||
[template.recipients],
|
||||
);
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ import { NameField } from '~/app/(signing)/sign/[token]/name-field';
|
||||
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { RadioField } from '~/app/(signing)/sign/[token]/radio-field';
|
||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||
import { SignDialog } from '~/app/(signing)/sign/[token]/sign-dialog';
|
||||
import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
||||
@ -169,7 +170,7 @@ export const SignDirectTemplateForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecipientProvider recipient={directRecipient}>
|
||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
@ -186,7 +187,6 @@ export const SignDirectTemplateForm = ({
|
||||
<SignatureField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -195,7 +195,6 @@ export const SignDirectTemplateForm = ({
|
||||
<InitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -204,7 +203,6 @@ export const SignDirectTemplateForm = ({
|
||||
<NameField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -213,7 +211,6 @@ export const SignDirectTemplateForm = ({
|
||||
<DateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||
onSignField={onSignField}
|
||||
@ -224,7 +221,6 @@ export const SignDirectTemplateForm = ({
|
||||
<EmailField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -241,7 +237,6 @@ export const SignDirectTemplateForm = ({
|
||||
...field,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
}}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -259,7 +254,6 @@ export const SignDirectTemplateForm = ({
|
||||
...field,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
}}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -277,7 +271,6 @@ export const SignDirectTemplateForm = ({
|
||||
...field,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
}}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -295,7 +288,6 @@ export const SignDirectTemplateForm = ({
|
||||
...field,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
}}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -313,7 +305,6 @@ export const SignDirectTemplateForm = ({
|
||||
...field,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
}}
|
||||
recipient={directRecipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -383,6 +374,6 @@ export const SignDirectTemplateForm = ({
|
||||
/>
|
||||
</div>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</>
|
||||
</RecipientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
|
||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
||||
|
||||
type ConfirmationDialogProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
hasUninsertedFields: boolean;
|
||||
isSubmitting: boolean;
|
||||
};
|
||||
|
||||
export function AssistantConfirmationDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
hasUninsertedFields,
|
||||
isSubmitting,
|
||||
}: ConfirmationDialogProps) {
|
||||
const onOpenChange = () => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Complete Document</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Are you sure you want to complete the document? This action cannot be undone. Please
|
||||
ensure that you have completed prefilling all relevant fields before proceeding.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<SigningDisclosure />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||
onClick={onConfirm}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : hasUninsertedFields ? 'Proceed' : 'Continue'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -13,7 +13,6 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -27,23 +26,19 @@ import { Label } from '@documenso/ui/primitives/label';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type CheckboxFieldProps = {
|
||||
field: FieldWithSignatureAndFieldMeta;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const CheckboxField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: CheckboxFieldProps) => {
|
||||
export const CheckboxField = ({ field, onSignField, onUnsignField }: CheckboxFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
@ -122,7 +117,9 @@ export const CheckboxField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -151,7 +148,7 @@ export const CheckboxField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -183,28 +180,25 @@ export const CheckboxField = ({
|
||||
...checkedValues,
|
||||
item.value.length > 0 ? item.value : `empty-value-${item.id}`,
|
||||
];
|
||||
|
||||
await removeSignedFieldWithToken({
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
});
|
||||
|
||||
if (isLengthConditionMet) {
|
||||
await signFieldWithToken({
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value: toCheckboxValue(checkedValues),
|
||||
isBase64: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
updatedValues = checkedValues.filter(
|
||||
(v) => v !== item.value && v !== `empty-value-${item.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
await removeSignedFieldWithToken({
|
||||
setCheckedValues(updatedValues);
|
||||
|
||||
await removeSignedFieldWithToken({
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
});
|
||||
|
||||
if (updatedValues.length > 0) {
|
||||
await signFieldWithToken({
|
||||
token: recipient.token,
|
||||
fieldId: field.id,
|
||||
value: toCheckboxValue(updatedValues),
|
||||
isBase64: true,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -216,7 +210,6 @@ export const CheckboxField = ({
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setCheckedValues(updatedValues);
|
||||
startTransition(() => router.refresh());
|
||||
}
|
||||
};
|
||||
|
||||
@ -17,7 +17,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZDateFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -27,11 +26,11 @@ import type {
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type DateFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
dateFormat?: string | null;
|
||||
timezone?: string | null;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
@ -40,17 +39,17 @@ export type DateFieldProps = {
|
||||
|
||||
export const DateField = ({
|
||||
field,
|
||||
recipient,
|
||||
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: DateFieldProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||
@ -67,9 +66,7 @@ export const DateField = ({
|
||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||
|
||||
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
||||
|
||||
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
||||
|
||||
const tooltipText = _(
|
||||
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`,
|
||||
);
|
||||
@ -102,7 +99,9 @@ export const DateField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -128,7 +127,7 @@ export const DateField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -30,23 +29,19 @@ import {
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type DropdownFieldProps = {
|
||||
field: FieldWithSignatureAndFieldMeta;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const DropdownField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: DropdownFieldProps) => {
|
||||
export const DropdownField = ({ field, onSignField, onUnsignField }: DropdownFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
@ -103,7 +98,9 @@ export const DropdownField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -134,7 +131,7 @@ export const DropdownField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -23,22 +22,23 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type EmailFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
|
||||
export const EmailField = ({ field, onSignField, onUnsignField }: EmailFieldProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { email: providedEmail } = useRequiredSigningContext();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
@ -86,7 +86,9 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -112,7 +114,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useId, useMemo, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import { type Field, FieldType, type Recipient, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -21,8 +24,11 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { AssistantConfirmationDialog } from './assistant/assistant-confirmation-dialog';
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { SignDialog } from './sign-dialog';
|
||||
|
||||
@ -32,6 +38,8 @@ export type SigningFormProps = {
|
||||
fields: Field[];
|
||||
redirectUrl?: string | null;
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
setSelectedSignerId?: (id: number | null) => void;
|
||||
};
|
||||
|
||||
export const SigningForm = ({
|
||||
@ -40,19 +48,35 @@ export const SigningForm = ({
|
||||
fields,
|
||||
redirectUrl,
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
setSelectedSignerId,
|
||||
}: SigningFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const { data: session } = useSession();
|
||||
|
||||
const assistantSignersId = useId();
|
||||
|
||||
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
|
||||
useRequiredSigningContext();
|
||||
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
||||
const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
|
||||
defaultValues: {
|
||||
selectedSignerId: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, formState } = useForm();
|
||||
|
||||
// Keep the loading state going if successful since the redirect may take some time.
|
||||
@ -67,7 +91,11 @@ export const SigningForm = ({
|
||||
|
||||
const uninsertedFields = useMemo(() => {
|
||||
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
|
||||
}, [fields]);
|
||||
}, [fieldsRequiringValidation]);
|
||||
|
||||
const uninsertedRecipientFields = useMemo(() => {
|
||||
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
|
||||
}, [fieldsRequiringValidation, recipient]);
|
||||
|
||||
const fieldsValidated = () => {
|
||||
setValidateUninsertedFields(true);
|
||||
@ -88,12 +116,31 @@ export const SigningForm = ({
|
||||
}
|
||||
|
||||
await completeDocument();
|
||||
};
|
||||
|
||||
// Reauth is currently not required for completing the document.
|
||||
// await executeActionAuthProcedure({
|
||||
// onReauthFormSubmit: completeDocument,
|
||||
// actionTarget: 'DOCUMENT',
|
||||
// });
|
||||
const onAssistantFormSubmit = () => {
|
||||
if (uninsertedRecipientFields.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleAssistantConfirmDialogSubmit = async () => {
|
||||
setIsAssistantSubmitting(true);
|
||||
|
||||
try {
|
||||
await completeDocument();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while completing the document. Please try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
setIsAssistantSubmitting(false);
|
||||
setIsConfirmationDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
||||
@ -113,7 +160,7 @@ export const SigningForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className={cn(
|
||||
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
||||
{
|
||||
@ -121,7 +168,6 @@ export const SigningForm = ({
|
||||
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
|
||||
},
|
||||
)}
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{validateUninsertedFields && uninsertedFields[0] && (
|
||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||
@ -129,17 +175,13 @@ export const SigningForm = ({
|
||||
</FieldToolTip>
|
||||
)}
|
||||
|
||||
<fieldset
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex flex-1 flex-col')}>
|
||||
<div className="custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<h3 className="text-foreground text-2xl font-semibold">
|
||||
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
|
||||
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
|
||||
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
|
||||
{recipient.role === RecipientRole.ASSISTANT && <Trans>Assist Document</Trans>}
|
||||
</h3>
|
||||
|
||||
{recipient.role === RecipientRole.VIEWER ? (
|
||||
@ -176,91 +218,185 @@ export const SigningForm = ({
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : recipient.role === RecipientRole.ASSISTANT ? (
|
||||
<>
|
||||
<form onSubmit={assistantForm.handleSubmit(onAssistantFormSubmit)}>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
Complete the fields for the following signers. Once reviewed, they will inform
|
||||
you if any modifications are needed.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border my-4" />
|
||||
|
||||
<fieldset className="dark:bg-background border-border rounded-2xl border bg-white p-3">
|
||||
<Controller
|
||||
name="selectedSignerId"
|
||||
control={assistantForm.control}
|
||||
rules={{ required: 'Please select a signer' }}
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
className="gap-0 space-y-3 shadow-none"
|
||||
value={field.value?.toString()}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setSelectedSignerId?.(Number(value));
|
||||
}}
|
||||
>
|
||||
{allRecipients
|
||||
.filter((r) => r.fields.length > 0)
|
||||
.map((r) => (
|
||||
<div
|
||||
key={`${assistantSignersId}-${r.id}`}
|
||||
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem
|
||||
id={`${assistantSignersId}-${r.id}`}
|
||||
value={r.id.toString()}
|
||||
className="after:absolute after:inset-0"
|
||||
/>
|
||||
|
||||
<div className="grid grow gap-1">
|
||||
<Label
|
||||
className="inline-flex items-start"
|
||||
htmlFor={`${assistantSignersId}-${r.id}`}
|
||||
>
|
||||
{r.name}
|
||||
|
||||
{r.id === recipient.id && (
|
||||
<span className="text-muted-foreground ml-2">
|
||||
{_(msg`(You)`)}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">{r.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs leading-[inherit]">
|
||||
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
loading={isAssistantSubmitting}
|
||||
disabled={isAssistantSubmitting || uninsertedRecipientFields.length > 0}
|
||||
>
|
||||
{isAssistantSubmitting ? <Trans>Submitting...</Trans> : <Trans>Continue</Trans>}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AssistantConfirmationDialog
|
||||
hasUninsertedFields={uninsertedFields.length > 0}
|
||||
isOpen={isConfirmationDialogOpen}
|
||||
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
|
||||
onConfirm={handleAssistantConfirmDialogSubmit}
|
||||
isSubmitting={isAssistantSubmitting}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Please review the document before signing.</Trans>
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Please review the document before signing.</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
|
||||
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
<fieldset
|
||||
disabled={isSubmitting}
|
||||
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value.trimStart())}
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value.trimStart())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (signatureValid) {
|
||||
setSignature(value);
|
||||
}
|
||||
}}
|
||||
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<SignDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (signatureValid) {
|
||||
setSignature(value);
|
||||
}
|
||||
}}
|
||||
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<SignDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -22,26 +21,22 @@ import type {
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type InitialsFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const InitialsField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
}: InitialsFieldProps) => {
|
||||
export const InitialsField = ({ field, onSignField, onUnsignField }: InitialsFieldProps) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { fullName } = useRequiredSigningContext();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
const initials = extractInitials(fullName);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
@ -87,7 +82,9 @@ export const InitialsField = ({
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZNameFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import { type Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -28,16 +27,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type NameFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
|
||||
export const NameField = ({ field, onSignField, onUnsignField }: NameFieldProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
@ -45,6 +44,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
|
||||
const { fullName: providedFullName, setFullName: setProvidedFullName } =
|
||||
useRequiredSigningContext();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
||||
|
||||
@ -67,7 +67,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
const [localFullName, setLocalFullName] = useState('');
|
||||
|
||||
const onPreSign = () => {
|
||||
if (!providedFullName) {
|
||||
if (!providedFullName && !isAssistantMode) {
|
||||
setShowFullNameModal(true);
|
||||
return false;
|
||||
}
|
||||
@ -90,9 +90,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
|
||||
const onSign = async (authOptions?: TRecipientActionAuth, name?: string) => {
|
||||
try {
|
||||
const value = name || providedFullName;
|
||||
const value = name || providedFullName || '';
|
||||
|
||||
if (!value) {
|
||||
if (!value && !isAssistantMode) {
|
||||
setShowFullNameModal(true);
|
||||
return;
|
||||
}
|
||||
@ -124,7 +124,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -150,7 +152,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -27,6 +26,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
type ValidationErrors = {
|
||||
@ -39,18 +39,18 @@ type ValidationErrors = {
|
||||
|
||||
export type NumberFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
|
||||
export const NumberField = ({ field, onSignField, onUnsignField }: NumberFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [showRadioModal, setShowRadioModal] = useState(false);
|
||||
const [showNumberModal, setShowNumberModal] = useState(false);
|
||||
|
||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||
@ -105,7 +105,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
};
|
||||
|
||||
const onDialogSignClick = () => {
|
||||
setShowRadioModal(false);
|
||||
setShowNumberModal(false);
|
||||
|
||||
void executeActionAuthProcedure({
|
||||
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
|
||||
@ -148,14 +148,20 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onPreSign = () => {
|
||||
setShowRadioModal(true);
|
||||
if (isAssistantMode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
setShowNumberModal(true);
|
||||
|
||||
if (localNumber && parsedFieldMeta) {
|
||||
const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true);
|
||||
@ -173,8 +179,14 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
|
||||
const onRemove = async () => {
|
||||
try {
|
||||
if (isAssistantMode && !targetSigner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
||||
|
||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
token: signingRecipient.token,
|
||||
fieldId: field.id,
|
||||
};
|
||||
|
||||
@ -193,18 +205,18 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showRadioModal) {
|
||||
if (!showNumberModal) {
|
||||
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
|
||||
setErrors(initialErrors);
|
||||
}
|
||||
}, [showRadioModal]);
|
||||
}, [showNumberModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -222,8 +234,8 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
|
||||
if (parsedFieldMeta?.label) {
|
||||
fieldDisplayName =
|
||||
parsedFieldMeta.label.length > 10
|
||||
? parsedFieldMeta.label.substring(0, 10) + '...'
|
||||
parsedFieldMeta.label.length > 20
|
||||
? parsedFieldMeta.label.substring(0, 20) + '...'
|
||||
: parsedFieldMeta.label;
|
||||
}
|
||||
|
||||
@ -235,7 +247,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
onPreSign={onPreSign}
|
||||
onSign={onSign}
|
||||
onRemove={onRemove}
|
||||
type="Signature"
|
||||
type="Number"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||
@ -278,7 +290,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
|
||||
<Dialog open={showNumberModal} onOpenChange={setShowNumberModal}>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
|
||||
@ -334,7 +346,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowRadioModal(false);
|
||||
setShowNumberModal(false);
|
||||
setLocalNumber('');
|
||||
}}
|
||||
>
|
||||
|
||||
@ -12,11 +12,12 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||
import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { DocumentAuthProvider } from './document-auth-provider';
|
||||
import { NoLongerAvailable } from './no-longer-available';
|
||||
@ -43,14 +44,14 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
|
||||
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||
|
||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||
const [document, recipient, fields, completedFields] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
userId: user?.id,
|
||||
requireAccessAuth: false,
|
||||
}).catch(() => null),
|
||||
getFieldsForToken({ token }),
|
||||
getRecipientByToken({ token }).catch(() => null),
|
||||
getFieldsForToken({ token }),
|
||||
getCompletedFieldsForToken({ token }),
|
||||
]);
|
||||
|
||||
@ -63,12 +64,21 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const recipientWithFields = { ...recipient, fields };
|
||||
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
|
||||
|
||||
if (!isRecipientsTurn) {
|
||||
return redirect(`/sign/${token}/waiting`);
|
||||
}
|
||||
|
||||
const allRecipients =
|
||||
recipient.role === RecipientRole.ASSISTANT
|
||||
? await getRecipientsForAssistant({
|
||||
token,
|
||||
})
|
||||
: [];
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
@ -153,11 +163,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
user={user}
|
||||
>
|
||||
<SigningPageView
|
||||
recipient={recipient}
|
||||
recipient={recipientWithFields}
|
||||
document={document}
|
||||
fields={fields}
|
||||
completedFields={completedFields}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
/>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
|
||||
@ -12,7 +12,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -24,18 +23,19 @@ import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
export type RadioFieldProps = {
|
||||
field: FieldWithSignatureAndFieldMeta;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const RadioField = ({ field, recipient, onSignField, onUnsignField }: RadioFieldProps) => {
|
||||
export const RadioField = ({ field, onSignField, onUnsignField }: RadioFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
@ -68,16 +68,26 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
|
||||
|
||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||
try {
|
||||
if (isAssistantMode && !targetSigner) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
||||
|
||||
const payload: TSignFieldWithTokenMutationSchema = {
|
||||
token: recipient.token,
|
||||
token: signingRecipient.token,
|
||||
fieldId: field.id,
|
||||
value: selectedOption,
|
||||
isBase64: true,
|
||||
authOptions,
|
||||
...(isAssistantMode && {
|
||||
isAssistantPrefill: true,
|
||||
assistantId: recipient.id,
|
||||
}),
|
||||
};
|
||||
|
||||
if (onSignField) {
|
||||
@ -99,7 +109,9 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -126,7 +138,7 @@ export const RadioField = ({ field, recipient, onSignField, onUnsignField }: Rad
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the signature.`),
|
||||
description: _(msg`An error occurred while removing the selection.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import { type PropsWithChildren, createContext, useContext } from 'react';
|
||||
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
|
||||
export interface RecipientContextValue {
|
||||
/**
|
||||
* The recipient who is currently signing the document.
|
||||
* In regular mode, this is the actual signer.
|
||||
* In assistant mode, this is the recipient who is helping fill out the document.
|
||||
*/
|
||||
recipient: Recipient | RecipientWithFields;
|
||||
|
||||
/**
|
||||
* Only present in assistant mode.
|
||||
* The recipient on whose behalf we're filling out the document.
|
||||
*/
|
||||
targetSigner: RecipientWithFields | null;
|
||||
|
||||
/**
|
||||
* Whether we're in assistant mode (one recipient filling out for another)
|
||||
*/
|
||||
isAssistantMode: boolean;
|
||||
}
|
||||
|
||||
const RecipientContext = createContext<RecipientContextValue | null>(null);
|
||||
|
||||
export interface RecipientProviderProps extends PropsWithChildren {
|
||||
recipient: Recipient | RecipientWithFields;
|
||||
targetSigner?: RecipientWithFields | null;
|
||||
}
|
||||
|
||||
export const RecipientProvider = ({
|
||||
children,
|
||||
recipient,
|
||||
targetSigner = null,
|
||||
}: RecipientProviderProps) => {
|
||||
// console.log({
|
||||
// recipient,
|
||||
// targetSigner,
|
||||
// isAssistantMode: !!targetSigner,
|
||||
// });
|
||||
return (
|
||||
<RecipientContext.Provider
|
||||
value={{
|
||||
recipient,
|
||||
targetSigner,
|
||||
isAssistantMode: !!targetSigner,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecipientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useRecipientContext() {
|
||||
const context = useContext(RecipientContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useRecipientContext must be used within a RecipientProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@ -11,7 +11,6 @@ import { Loader } from 'lucide-react';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { type Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -28,12 +27,12 @@ import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRequiredSigningContext } from './provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
|
||||
export type SignatureFieldProps = {
|
||||
field: FieldWithSignature;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
typedSignatureEnabled?: boolean;
|
||||
@ -41,15 +40,14 @@ export type SignatureFieldProps = {
|
||||
|
||||
export const SignatureField = ({
|
||||
field,
|
||||
recipient,
|
||||
onSignField,
|
||||
onUnsignField,
|
||||
typedSignatureEnabled,
|
||||
}: SignatureFieldProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { recipient } = useRecipientContext();
|
||||
|
||||
const signatureRef = useRef<HTMLParagraphElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -46,6 +46,7 @@ export type SignatureFieldProps = {
|
||||
| 'Email'
|
||||
| 'Name'
|
||||
| 'Signature'
|
||||
| 'Text'
|
||||
| 'Radio'
|
||||
| 'Dropdown'
|
||||
| 'Number'
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -13,9 +17,10 @@ import {
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
@ -32,16 +37,18 @@ import { InitialsField } from './initials-field';
|
||||
import { NameField } from './name-field';
|
||||
import { NumberField } from './number-field';
|
||||
import { RadioField } from './radio-field';
|
||||
import { RecipientProvider } from './recipient-context';
|
||||
import { RejectDocumentDialog } from './reject-document-dialog';
|
||||
import { SignatureField } from './signature-field';
|
||||
import { TextField } from './text-field';
|
||||
|
||||
export type SigningPageViewProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: Recipient;
|
||||
recipient: RecipientWithFields;
|
||||
fields: Field[];
|
||||
completedFields: CompletedField[];
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
};
|
||||
|
||||
export const SigningPageView = ({
|
||||
@ -50,9 +57,12 @@ export const SigningPageView = ({
|
||||
fields,
|
||||
completedFields,
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
}: SigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||
|
||||
const shouldUseTeamDetails =
|
||||
document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
|
||||
|
||||
@ -64,153 +74,162 @@ export const SigningPageView = ({
|
||||
senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : '';
|
||||
}
|
||||
|
||||
const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl">
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-x-6">
|
||||
<div className="max-w-[50ch]">
|
||||
<span className="text-muted-foreground truncate" title={senderName}>
|
||||
{senderName} {senderEmail}
|
||||
</span>{' '}
|
||||
<span className="text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to view this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to view this document</Trans>
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.SIGNER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to sign this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to sign this document</Trans>
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.APPROVER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to approve this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to approve this document</Trans>
|
||||
),
|
||||
)
|
||||
.otherwise(() => null)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RejectDocumentDialog document={document} token={recipient.token} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
|
||||
<Card
|
||||
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
||||
gradient
|
||||
<RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
|
||||
<div className="mx-auto w-full max-w-screen-xl">
|
||||
<h1
|
||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer
|
||||
key={documentData.id}
|
||||
documentData={documentData}
|
||||
document={document}
|
||||
password={documentMeta?.password}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
||||
<SigningForm
|
||||
document={document}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
redirectUrl={documentMeta?.redirectUrl}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
/>
|
||||
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-x-6">
|
||||
<div className="max-w-[50ch]">
|
||||
<span className="text-muted-foreground truncate" title={senderName}>
|
||||
{senderName} {senderEmail}
|
||||
</span>{' '}
|
||||
<span className="text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to view this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to view this document</Trans>
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.SIGNER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to sign this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to sign this document</Trans>
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.APPROVER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to approve this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to approve this document</Trans>
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.ASSISTANT, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to assist this document
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>has invited you to assist this document</Trans>
|
||||
),
|
||||
)
|
||||
.otherwise(() => null)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RejectDocumentDialog document={document} token={recipient.token} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentReadOnlyFields fields={completedFields} />
|
||||
|
||||
<AutoSign recipient={recipient} fields={fields} />
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.SIGNATURE, () => (
|
||||
<SignatureField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
|
||||
<div className="mt-8 grid grid-cols-12 gap-y-8 lg:gap-x-8 lg:gap-y-0">
|
||||
<Card
|
||||
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-7 xl:col-span-8"
|
||||
gradient
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<LazyPDFViewer
|
||||
key={documentData.id}
|
||||
documentData={documentData}
|
||||
document={document}
|
||||
password={documentMeta?.password}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.INITIALS, () => (
|
||||
<InitialsField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.NAME, () => (
|
||||
<NameField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.DATE, () => (
|
||||
<DateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.EMAIL, () => (
|
||||
<EmailField key={field.id} field={field} recipient={recipient} />
|
||||
))
|
||||
.with(FieldType.TEXT, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <TextField key={field.id} field={fieldWithMeta} recipient={recipient} />;
|
||||
})
|
||||
.with(FieldType.NUMBER, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <NumberField key={field.id} field={fieldWithMeta} recipient={recipient} />;
|
||||
})
|
||||
.with(FieldType.RADIO, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <RadioField key={field.id} field={fieldWithMeta} recipient={recipient} />;
|
||||
})
|
||||
.with(FieldType.CHECKBOX, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <CheckboxField key={field.id} field={fieldWithMeta} recipient={recipient} />;
|
||||
})
|
||||
.with(FieldType.DROPDOWN, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <DropdownField key={field.id} field={fieldWithMeta} recipient={recipient} />;
|
||||
})
|
||||
.otherwise(() => null),
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="col-span-12 lg:col-span-5 xl:col-span-4">
|
||||
<SigningForm
|
||||
document={document}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
redirectUrl={documentMeta?.redirectUrl}
|
||||
isRecipientsTurn={isRecipientsTurn}
|
||||
allRecipients={allRecipients}
|
||||
setSelectedSignerId={setSelectedSignerId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentReadOnlyFields fields={completedFields} />
|
||||
|
||||
{recipient.role !== RecipientRole.ASSISTANT && (
|
||||
<AutoSign recipient={recipient} fields={fields} />
|
||||
)}
|
||||
</ElementVisible>
|
||||
</div>
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields
|
||||
.filter(
|
||||
(field) =>
|
||||
recipient.role !== RecipientRole.ASSISTANT ||
|
||||
field.recipientId === selectedSigner?.id,
|
||||
)
|
||||
.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.SIGNATURE, () => <SignatureField key={field.id} field={field} />)
|
||||
.with(FieldType.INITIALS, () => <InitialsField key={field.id} field={field} />)
|
||||
.with(FieldType.NAME, () => <NameField key={field.id} field={field} />)
|
||||
.with(FieldType.DATE, () => (
|
||||
<DateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||
/>
|
||||
))
|
||||
.with(FieldType.EMAIL, () => <EmailField key={field.id} field={field} />)
|
||||
.with(FieldType.TEXT, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <TextField key={field.id} field={fieldWithMeta} />;
|
||||
})
|
||||
.with(FieldType.NUMBER, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <NumberField key={field.id} field={fieldWithMeta} />;
|
||||
})
|
||||
.with(FieldType.RADIO, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <RadioField key={field.id} field={fieldWithMeta} />;
|
||||
})
|
||||
.with(FieldType.CHECKBOX, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <CheckboxField key={field.id} field={fieldWithMeta} />;
|
||||
})
|
||||
.with(FieldType.DROPDOWN, () => {
|
||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||
...field,
|
||||
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
|
||||
};
|
||||
return <DropdownField key={field.id} field={fieldWithMeta} />;
|
||||
})
|
||||
.otherwise(() => null),
|
||||
)}
|
||||
</ElementVisible>
|
||||
</div>
|
||||
</RecipientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -13,7 +13,6 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/tr
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import { ZTextFieldMeta } from '@documenso/lib/types/field-meta';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
@ -27,26 +26,31 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||
import { useRecipientContext } from './recipient-context';
|
||||
import { SigningFieldContainer } from './signing-field-container';
|
||||
|
||||
type ValidationErrors = {
|
||||
required: string[];
|
||||
characterLimit: string[];
|
||||
};
|
||||
|
||||
export type TextFieldProps = {
|
||||
field: FieldWithSignatureAndFieldMeta;
|
||||
recipient: Recipient;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const TextField = ({ field, recipient, onSignField, onUnsignField }: TextFieldProps) => {
|
||||
export const TextField = ({ field, onSignField, onUnsignField }: TextFieldProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { recipient, targetSigner, isAssistantMode } = useRecipientContext();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const initialErrors: Record<string, string[]> = {
|
||||
const initialErrors: ValidationErrors = {
|
||||
required: [],
|
||||
characterLimit: [],
|
||||
};
|
||||
|
||||
const [errors, setErrors] = useState(initialErrors);
|
||||
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
|
||||
|
||||
@ -166,7 +170,9 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
description: isAssistantMode
|
||||
? _(msg`An error occurred while signing as assistant.`)
|
||||
: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -194,7 +200,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while removing the text.`),
|
||||
description: _(msg`An error occurred while removing the field.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -234,7 +240,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
onPreSign={onPreSign}
|
||||
onSign={onSign}
|
||||
onRemove={onRemove}
|
||||
type="Signature"
|
||||
type="Text"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||
@ -276,7 +282,7 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
||||
>
|
||||
{field.customText.length < 20
|
||||
? field.customText
|
||||
: field.customText.substring(0, 15) + '...'}
|
||||
: field.customText.substring(0, 20) + '...'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -485,7 +485,6 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
|
||||
{/* Fields */}
|
||||
<EmbedDocumentFields
|
||||
recipient={recipient}
|
||||
fields={localFields}
|
||||
metadata={metadata}
|
||||
onSignField={onSignField}
|
||||
|
||||
@ -13,6 +13,7 @@ import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
|
||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||
|
||||
import { EmbedAuthenticateView } from '../../authenticate';
|
||||
import { EmbedPaywall } from '../../paywall';
|
||||
@ -96,16 +97,18 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
>
|
||||
<EmbedDirectTemplateClientPage
|
||||
token={token}
|
||||
updatedAt={template.updatedAt}
|
||||
documentData={template.templateDocumentData}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={template.templateMeta}
|
||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||
/>
|
||||
<RecipientProvider recipient={recipient}>
|
||||
<EmbedDirectTemplateClientPage
|
||||
token={token}
|
||||
updatedAt={template.updatedAt}
|
||||
documentData={template.templateDocumentData}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={template.templateMeta}
|
||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||
/>
|
||||
</RecipientProvider>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '@documenso/lib/types/field-meta';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { type Field, FieldType } from '@documenso/prisma/client';
|
||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||
import type {
|
||||
@ -33,7 +33,6 @@ import { SignatureField } from '~/app/(signing)/sign/[token]/signature-field';
|
||||
import { TextField } from '~/app/(signing)/sign/[token]/text-field';
|
||||
|
||||
export type EmbedDocumentFieldsProps = {
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||
@ -41,7 +40,6 @@ export type EmbedDocumentFieldsProps = {
|
||||
};
|
||||
|
||||
export const EmbedDocumentFields = ({
|
||||
recipient,
|
||||
fields,
|
||||
metadata,
|
||||
onSignField,
|
||||
@ -55,7 +53,6 @@ export const EmbedDocumentFields = ({
|
||||
<SignatureField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||
@ -65,7 +62,6 @@ export const EmbedDocumentFields = ({
|
||||
<InitialsField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -74,7 +70,6 @@ export const EmbedDocumentFields = ({
|
||||
<NameField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -83,7 +78,6 @@ export const EmbedDocumentFields = ({
|
||||
<DateField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||
@ -94,7 +88,6 @@ export const EmbedDocumentFields = ({
|
||||
<EmailField
|
||||
key={field.id}
|
||||
field={field}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -109,7 +102,6 @@ export const EmbedDocumentFields = ({
|
||||
<TextField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -125,7 +117,6 @@ export const EmbedDocumentFields = ({
|
||||
<NumberField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -141,7 +132,6 @@ export const EmbedDocumentFields = ({
|
||||
<RadioField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -157,7 +147,6 @@ export const EmbedDocumentFields = ({
|
||||
<CheckboxField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
@ -173,7 +162,6 @@ export const EmbedDocumentFields = ({
|
||||
<DropdownField
|
||||
key={field.id}
|
||||
field={fieldWithMeta}
|
||||
recipient={recipient}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useEffect, useId, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
@ -9,8 +9,9 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
|
||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -19,10 +20,12 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
@ -35,12 +38,13 @@ export type EmbedSignDocumentClientPageProps = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
documentData: DocumentData;
|
||||
recipient: Recipient;
|
||||
recipient: RecipientWithFields;
|
||||
fields: Field[];
|
||||
metadata?: DocumentMeta | TemplateMeta | null;
|
||||
isCompleted?: boolean;
|
||||
hidePoweredBy?: boolean;
|
||||
isPlatformOrEnterprise?: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
};
|
||||
|
||||
export const EmbedSignDocumentClientPage = ({
|
||||
@ -53,6 +57,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
isCompleted,
|
||||
hidePoweredBy = false,
|
||||
isPlatformOrEnterprise = false,
|
||||
allRecipients = [],
|
||||
}: EmbedSignDocumentClientPageProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@ -70,17 +75,21 @@ export const EmbedSignDocumentClientPage = ({
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||
);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||
|
||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||
|
||||
const [pendingFields, _completedFields] = [
|
||||
fields.filter((field) => !field.inserted),
|
||||
fields.filter((field) => field.recipientId === recipient.id && !field.inserted),
|
||||
fields.filter((field) => field.inserted),
|
||||
];
|
||||
|
||||
@ -89,6 +98,8 @@ export const EmbedSignDocumentClientPage = ({
|
||||
|
||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||
|
||||
const assistantSignersId = useId();
|
||||
|
||||
const onNextFieldClick = () => {
|
||||
validateFieldsInserted(fields);
|
||||
|
||||
@ -214,164 +225,234 @@ export const EmbedSignDocumentClientPage = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||
<RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||
|
||||
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||
{/* Viewer */}
|
||||
<div className="flex-1">
|
||||
<LazyPDFViewer
|
||||
documentData={documentData}
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||
{/* Viewer */}
|
||||
<div className="flex-1">
|
||||
<LazyPDFViewer
|
||||
documentData={documentData}
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Widget */}
|
||||
<div
|
||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||
<Trans>Sign document</Trans>
|
||||
</h3>
|
||||
{/* Widget */}
|
||||
<div
|
||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||
{isAssistantMode ? (
|
||||
<Trans>Assist with signing</Trans>
|
||||
) : (
|
||||
<Trans>Sign document</Trans>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Sign the document to complete the process.</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
disabled={isNameLocked}
|
||||
value={fullName}
|
||||
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">
|
||||
<Trans>Email</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
className="bg-background mt-2"
|
||||
value={email}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
allowTypedSignature={Boolean(
|
||||
metadata &&
|
||||
'typedSignatureEnabled' in metadata &&
|
||||
metadata.typedSignatureEnabled,
|
||||
)}
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{isAssistantMode ? (
|
||||
<Trans>Help complete the document for other signers.</Trans>
|
||||
) : (
|
||||
<Trans>Sign the document to complete the process.</Trans>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
{isAssistantMode && (
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>Signing for</Trans>
|
||||
</Label>
|
||||
|
||||
<fieldset className="dark:bg-background border-border mt-2 rounded-2xl border bg-white p-3">
|
||||
<RadioGroup
|
||||
className="gap-0 space-y-3 shadow-none"
|
||||
value={selectedSignerId?.toString()}
|
||||
onValueChange={(value) => setSelectedSignerId(Number(value))}
|
||||
>
|
||||
{allRecipients
|
||||
.filter((r) => r.fields.length > 0)
|
||||
.map((r) => (
|
||||
<div
|
||||
key={`${assistantSignersId}-${r.id}`}
|
||||
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem
|
||||
id={`${assistantSignersId}-${r.id}`}
|
||||
value={r.id.toString()}
|
||||
className="after:absolute after:inset-0"
|
||||
/>
|
||||
|
||||
<div className="grid grow gap-1">
|
||||
<Label
|
||||
className="inline-flex items-start"
|
||||
htmlFor={`${assistantSignersId}-${r.id}`}
|
||||
>
|
||||
{r.name}
|
||||
|
||||
{r.id === recipient.id && (
|
||||
<span className="text-muted-foreground ml-2">
|
||||
{_(msg`(You)`)}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">{r.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs leading-[inherit]">
|
||||
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAssistantMode && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
disabled={isNameLocked}
|
||||
value={fullName}
|
||||
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">
|
||||
<Trans>Email</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
className="bg-background mt-2"
|
||||
value={email}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<Card className="mt-2" gradient degrees={-120}>
|
||||
<CardContent className="p-0">
|
||||
<SignaturePad
|
||||
className="h-44 w-full"
|
||||
disabled={isThrottled || isSubmitting}
|
||||
defaultValue={signature ?? undefined}
|
||||
onChange={(value) => {
|
||||
setSignature(value);
|
||||
}}
|
||||
onValidityChange={(isValid) => {
|
||||
setSignatureValid(isValid);
|
||||
}}
|
||||
allowTypedSignature={Boolean(
|
||||
metadata &&
|
||||
'typedSignatureEnabled' in metadata &&
|
||||
metadata.typedSignatureEnabled,
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{hasSignatureField && !signatureValid && (
|
||||
<div className="text-destructive mt-2 text-sm">
|
||||
<Trans>
|
||||
Signature is too small. Please provide a more complete signature.
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||
|
||||
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||
{pendingFields.length > 0 ? (
|
||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||
<Trans>Next</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="col-start-2"
|
||||
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
||||
loading={isSubmitting}
|
||||
onClick={() => throttledOnCompleteClick()}
|
||||
>
|
||||
<Trans>Complete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||
{pendingFields.length > 0 ? (
|
||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||
<Trans>Next</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="col-start-2"
|
||||
disabled={
|
||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||
}
|
||||
loading={isSubmitting}
|
||||
onClick={() => throttledOnCompleteClick()}
|
||||
>
|
||||
<Trans>Complete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
</ElementVisible>
|
||||
|
||||
{/* Fields */}
|
||||
<EmbedDocumentFields fields={fields} metadata={metadata} />
|
||||
</div>
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip key={pendingFields[0].id} field={pendingFields[0]} color="warning">
|
||||
<Trans>Click to insert field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
</ElementVisible>
|
||||
|
||||
{/* Fields */}
|
||||
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
|
||||
{!hidePoweredBy && (
|
||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||
<span>Powered by</span>
|
||||
<Logo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hidePoweredBy && (
|
||||
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||
<span>Powered by</span>
|
||||
<Logo className="ml-2 inline-block h-[14px]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</RecipientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,17 +8,20 @@ import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
|
||||
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
|
||||
|
||||
import { EmbedAuthenticateView } from '../../authenticate';
|
||||
import { EmbedPaywall } from '../../paywall';
|
||||
import { EmbedWaitingForTurn } from '../../waiting-for-turn';
|
||||
import { EmbedSignDocumentClientPage } from './client';
|
||||
|
||||
export type EmbedSignDocumentPageProps = {
|
||||
@ -85,6 +88,19 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
||||
);
|
||||
}
|
||||
|
||||
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
|
||||
|
||||
if (!isRecipientsTurnToSign) {
|
||||
return <EmbedWaitingForTurn />;
|
||||
}
|
||||
|
||||
const allRecipients =
|
||||
recipient.role === RecipientRole.ASSISTANT
|
||||
? await getRecipientsForAssistant({
|
||||
token,
|
||||
})
|
||||
: [];
|
||||
|
||||
const team = document.teamId
|
||||
? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
|
||||
: null;
|
||||
@ -112,6 +128,7 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
|
||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||
allRecipients={allRecipients}
|
||||
/>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
|
||||
48
apps/web/src/app/embed/waiting-for-turn.tsx
Normal file
48
apps/web/src/app/embed/waiting-for-turn.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
export const EmbedWaitingForTurn = () => {
|
||||
const [hasPostedMessage, setHasPostedMessage] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.parent && !hasPostedMessage) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-waiting-for-turn',
|
||||
data: null,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setHasPostedMessage(true);
|
||||
}, [hasPostedMessage]);
|
||||
|
||||
if (!hasPostedMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
<h3 className="text-foreground text-center text-2xl font-bold">
|
||||
<Trans>Waiting for Your Turn</Trans>
|
||||
</h3>
|
||||
|
||||
<div className="mt-8 max-w-[50ch] text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>
|
||||
It's currently not your turn to sign. Please check back soon as this document should be
|
||||
available for you to sign shortly.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
<Trans>Please check with the parent application for more information.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -85,7 +85,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
|
||||
const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
|
||||
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
||||
trpcReact.document.searchDocuments.useQuery(
|
||||
{
|
||||
query: search,
|
||||
|
||||
@ -67,7 +67,7 @@ export const TransferTeamDialog = ({
|
||||
const {
|
||||
data,
|
||||
refetch: refetchTeamMembers,
|
||||
isLoading: loadingTeamMembers,
|
||||
isPending: loadingTeamMembers,
|
||||
isLoadingError: loadingTeamMembersError,
|
||||
} = trpc.team.getTeamMembers.useQuery({
|
||||
teamId,
|
||||
|
||||
@ -353,6 +353,16 @@ export const DocumentHistorySheet = ({
|
||||
/>
|
||||
),
|
||||
)
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, ({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
{
|
||||
key: 'Field prefilled',
|
||||
value: formatGenericText(data.field.type),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))
|
||||
.exhaustive()}
|
||||
|
||||
{isUserDetailsVisible && (
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.0-rc.11",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.0-rc.11",
|
||||
"version": "1.9.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -106,7 +106,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.11",
|
||||
"version": "1.9.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.9.0-rc.11",
|
||||
"version": "1.9.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
|
||||
@ -540,12 +540,19 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
||||
if (i > 1) {
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
}
|
||||
|
||||
await page
|
||||
.getByPlaceholder('Email')
|
||||
.getByLabel('Email')
|
||||
.nth(i - 1)
|
||||
.focus();
|
||||
|
||||
await page
|
||||
.getByLabel('Email')
|
||||
.nth(i - 1)
|
||||
.fill(`user${i}@example.com`);
|
||||
|
||||
await page
|
||||
.getByPlaceholder('Name')
|
||||
.getByLabel('Name')
|
||||
.nth(i - 1)
|
||||
.fill(`User ${i}`);
|
||||
}
|
||||
|
||||
@ -84,6 +84,9 @@ export const TemplateDocumentInvite = ({
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Continue by viewing the document.</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Continue by approving the document.</Trans>)
|
||||
.with(RecipientRole.CC, () => '')
|
||||
.with(RecipientRole.ASSISTANT, () => (
|
||||
<Trans>Continue by assisting with the document.</Trans>
|
||||
))
|
||||
.exhaustive()}
|
||||
</Text>
|
||||
|
||||
@ -104,6 +107,7 @@ export const TemplateDocumentInvite = ({
|
||||
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
|
||||
.with(RecipientRole.CC, () => '')
|
||||
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
|
||||
.exhaustive()}
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@ -10,6 +10,9 @@ export const DOCUMENT_AUDIT_LOG_EMAIL_FORMAT = {
|
||||
[DOCUMENT_EMAIL_TYPE.APPROVE_REQUEST]: {
|
||||
description: 'Approval request',
|
||||
},
|
||||
[DOCUMENT_EMAIL_TYPE.ASSISTING_REQUEST]: {
|
||||
description: 'Assisting request',
|
||||
},
|
||||
[DOCUMENT_EMAIL_TYPE.CC]: {
|
||||
description: 'CC',
|
||||
},
|
||||
|
||||
@ -32,12 +32,26 @@ export const RECIPIENT_ROLES_DESCRIPTION = {
|
||||
roleName: msg`Viewer`,
|
||||
roleNamePlural: msg`Viewers`,
|
||||
},
|
||||
[RecipientRole.ASSISTANT]: {
|
||||
actionVerb: msg`Assist`,
|
||||
actioned: msg`Assisted`,
|
||||
progressiveVerb: msg`Assisting`,
|
||||
roleName: msg`Assistant`,
|
||||
roleNamePlural: msg`Assistants`,
|
||||
},
|
||||
} satisfies Record<keyof typeof RecipientRole, unknown>;
|
||||
|
||||
export const RECIPIENT_ROLE_TO_DISPLAY_TYPE = {
|
||||
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
|
||||
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
|
||||
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
|
||||
} as const;
|
||||
|
||||
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
|
||||
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
|
||||
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
|
||||
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
|
||||
[RecipientRole.ASSISTANT]: `ASSISTING_REQUEST`,
|
||||
} as const;
|
||||
|
||||
export const RECIPIENT_ROLE_SIGNING_REASONS = {
|
||||
@ -45,4 +59,5 @@ export const RECIPIENT_ROLE_SIGNING_REASONS = {
|
||||
[RecipientRole.APPROVER]: msg`I am an approver of this document`,
|
||||
[RecipientRole.CC]: msg`I am required to receive a copy of this document`,
|
||||
[RecipientRole.VIEWER]: msg`I am a viewer of this document`,
|
||||
[RecipientRole.ASSISTANT]: msg`I am an assistant of this document`,
|
||||
} satisfies Record<keyof typeof RecipientRole, MessageDescriptor>;
|
||||
|
||||
@ -14,8 +14,8 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
|
||||
@ -1,15 +1,55 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type GetFieldsForTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
|
||||
if (!token) {
|
||||
throw new Error('Missing token');
|
||||
}
|
||||
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: { token },
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
type: {
|
||||
not: FieldType.SIGNATURE,
|
||||
},
|
||||
recipient: {
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
},
|
||||
signingOrder: {
|
||||
gte: recipient.signingOrder ?? 0,
|
||||
},
|
||||
},
|
||||
documentId: recipient.documentId,
|
||||
},
|
||||
{
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
recipient: {
|
||||
token,
|
||||
},
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
include: {
|
||||
signature: true,
|
||||
|
||||
@ -4,7 +4,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type RemovedSignedFieldWithTokenOptions = {
|
||||
token: string;
|
||||
@ -17,11 +17,28 @@ export const removeSignedFieldWithToken = async ({
|
||||
fieldId,
|
||||
requestMetadata,
|
||||
}: RemovedSignedFieldWithTokenOptions) => {
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
recipient: {
|
||||
token,
|
||||
...(recipient.role !== RecipientRole.ASSISTANT
|
||||
? {
|
||||
id: recipient.id,
|
||||
}
|
||||
: {
|
||||
signingOrder: {
|
||||
gte: recipient.signingOrder ?? 0,
|
||||
},
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@ -30,7 +47,7 @@ export const removeSignedFieldWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const { document, recipient } = field;
|
||||
const { document } = field;
|
||||
|
||||
if (!document) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
@ -40,7 +57,10 @@ export const removeSignedFieldWithToken = async ({
|
||||
throw new Error(`Document ${document.id} must be pending`);
|
||||
}
|
||||
|
||||
if (recipient?.signingStatus === SigningStatus.SIGNED) {
|
||||
if (
|
||||
recipient?.signingStatus === SigningStatus.SIGNED ||
|
||||
field.recipient.signingStatus === SigningStatus.SIGNED
|
||||
) {
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
}
|
||||
|
||||
@ -66,20 +86,22 @@ export const removeSignedFieldWithToken = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
name: recipient?.name,
|
||||
email: recipient?.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
field: field.type,
|
||||
fieldId: field.secondaryId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (recipient.role !== RecipientRole.ASSISTANT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
field: field.type,
|
||||
fieldId: field.secondaryId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -10,7 +10,7 @@ import { validateRadioField } from '@documenso/lib/advanced-fields-validation/va
|
||||
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
|
||||
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||
@ -56,20 +56,41 @@ export const signFieldWithToken = async ({
|
||||
authOptions,
|
||||
requestMetadata,
|
||||
}: SignFieldWithTokenOptions) => {
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
const field = await prisma.field.findFirstOrThrow({
|
||||
where: {
|
||||
id: fieldId,
|
||||
recipient: {
|
||||
token,
|
||||
...(recipient.role !== RecipientRole.ASSISTANT
|
||||
? {
|
||||
id: recipient.id,
|
||||
}
|
||||
: {
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
},
|
||||
signingOrder: {
|
||||
gte: recipient.signingOrder ?? 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
document: {
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
},
|
||||
recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { document, recipient } = field;
|
||||
const { document } = field;
|
||||
|
||||
if (!document) {
|
||||
throw new Error(`Document not found for field ${field.id}`);
|
||||
@ -87,7 +108,10 @@ export const signFieldWithToken = async ({
|
||||
throw new Error(`Document ${document.id} must be pending for signing`);
|
||||
}
|
||||
|
||||
if (recipient?.signingStatus === SigningStatus.SIGNED) {
|
||||
if (
|
||||
recipient.signingStatus === SigningStatus.SIGNED ||
|
||||
field.recipient.signingStatus === SigningStatus.SIGNED
|
||||
) {
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
}
|
||||
|
||||
@ -183,6 +207,8 @@ export const signFieldWithToken = async ({
|
||||
throw new Error('Typed signatures are not allowed. Please draw your signature');
|
||||
}
|
||||
|
||||
const assistant = recipient.role === RecipientRole.ASSISTANT ? recipient : undefined;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
@ -219,11 +245,14 @@ export const signFieldWithToken = async ({
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
type:
|
||||
assistant && field.recipientId !== assistant.id
|
||||
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
|
||||
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
email: recipient.email,
|
||||
name: recipient.name,
|
||||
email: assistant?.email ?? recipient.email,
|
||||
name: assistant?.name ?? recipient.name,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
|
||||
@ -9,5 +9,8 @@ export const getRecipientByToken = async ({ token }: GetRecipientByTokenOptions)
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export interface GetRecipientsForAssistantOptions {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssistantOptions) => {
|
||||
const assistant = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!assistant) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Assistant not found',
|
||||
});
|
||||
}
|
||||
|
||||
let recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: assistant.documentId,
|
||||
signingOrder: {
|
||||
gte: assistant.signingOrder ?? 0,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
fields: {
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
recipientId: assistant.id,
|
||||
},
|
||||
{
|
||||
type: {
|
||||
not: FieldType.SIGNATURE,
|
||||
},
|
||||
documentId: assistant.documentId,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Omit the token for recipients other than the assistant so
|
||||
// it doesn't get sent to the client.
|
||||
recipients = recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
token: recipient.id === assistant.id ? token : '',
|
||||
}));
|
||||
|
||||
return recipients;
|
||||
};
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-01-19 12:04\n"
|
||||
"PO-Revision-Date: 2025-01-30 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -163,7 +163,7 @@ msgstr "{inviterName} hat dich aus dem Dokument<0/>\"{documentName}\" entfernt"
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:61
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{inviterName} im Namen von \"{teamName}\" hat dich eingeladen, {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:45
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
|
||||
@ -308,7 +308,7 @@ msgstr "{signerName} hat das Dokument \"{documentName}\" abgelehnt."
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:68
|
||||
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{teamName} hat dich eingeladen, {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:46
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
@ -1232,20 +1232,20 @@ msgstr "Bulk-Import"
|
||||
|
||||
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
|
||||
msgid "Bulk Send Complete: {0}"
|
||||
msgstr ""
|
||||
msgstr "Massenversand abgeschlossen: {0}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:30
|
||||
msgid "Bulk send operation complete for template \"{templateName}\""
|
||||
msgstr ""
|
||||
msgstr "Massenversand abgeschlossen für Vorlage \"{templateName}\""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
|
||||
msgid "Bulk Send Template via CSV"
|
||||
msgstr ""
|
||||
msgstr "Bulk-Vorlage senden über CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
|
||||
msgid "Bulk Send via CSV"
|
||||
msgstr ""
|
||||
msgstr "Massenversand per CSV"
|
||||
|
||||
#: packages/email/templates/team-invite.tsx:84
|
||||
msgid "by <0>{senderName}</0>"
|
||||
@ -1788,7 +1788,7 @@ msgstr "Erstellt am {0}"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
|
||||
msgid "CSV Structure"
|
||||
msgstr ""
|
||||
msgstr "CSV-Struktur"
|
||||
|
||||
#: apps/web/src/components/forms/password.tsx:112
|
||||
msgid "Current Password"
|
||||
@ -1800,7 +1800,7 @@ msgstr "Aktuelles Passwort ist falsch."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
|
||||
msgid "Current recipients:"
|
||||
msgstr ""
|
||||
msgstr "Aktuelle Empfänger:"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
|
||||
msgid "Daily"
|
||||
@ -2328,7 +2328,7 @@ msgstr "Zertifikat herunterladen"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
|
||||
msgid "Download Template CSV"
|
||||
msgstr ""
|
||||
msgstr "Vorlage CSV herunterladen"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
|
||||
#: apps/web/src/components/formatter/document-status.tsx:34
|
||||
@ -2659,7 +2659,7 @@ msgstr "Webhook konnte nicht aktualisiert werden"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:55
|
||||
msgid "Failed: {failedCount}"
|
||||
msgstr ""
|
||||
msgstr "Fehlgeschlagen: {failedCount}"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
|
||||
msgid "Field character limit"
|
||||
@ -2721,7 +2721,7 @@ msgstr "Für Fragen zu dieser Offenlegung, elektronischen Unterschriften oder ei
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
|
||||
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||
msgstr ""
|
||||
msgstr "Für jeden Empfänger geben Sie dessen E-Mail (erforderlich) und Namen (optional) in separaten Spalten an. Laden Sie unten die CSV-Vorlage für das korrekte Format herunter."
|
||||
|
||||
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
||||
msgid "Forgot Password?"
|
||||
@ -2834,7 +2834,7 @@ msgstr "Hey, ich bin Timur"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:36
|
||||
msgid "Hi {userName},"
|
||||
msgstr ""
|
||||
msgstr "Hallo, {userName},"
|
||||
|
||||
#: packages/email/templates/reset-password.tsx:56
|
||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
||||
@ -3238,7 +3238,7 @@ msgstr "Max"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
|
||||
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||
msgstr ""
|
||||
msgstr "Maximale Dateigröße: 4MB. Maximal 100 Zeilen pro Upload. Leere Werte verwenden die Vorlagenstandards."
|
||||
|
||||
#: packages/lib/constants/teams.ts:12
|
||||
msgid "Member"
|
||||
@ -3868,7 +3868,7 @@ msgstr "Bitte geben Sie <0>{0}</0> ein, um zu bestätigen."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr ""
|
||||
msgstr "Vorformatiertes CSV-Template mit Beispieldaten."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
|
||||
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
|
||||
@ -4392,7 +4392,7 @@ msgstr "Dokumente im Namen des Teams über die E-Mail-Adresse senden"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
|
||||
msgid "Send documents to recipients immediately"
|
||||
msgstr ""
|
||||
msgstr "Dokumente sofort an Empfänger senden"
|
||||
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
|
||||
msgid "Send on Behalf of Team"
|
||||
@ -4864,11 +4864,11 @@ msgstr "Passkey erfolgreich erstellt"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:52
|
||||
msgid "Successfully created: {successCount}"
|
||||
msgstr ""
|
||||
msgstr "Erfolgreich erstellt: {successCount}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:44
|
||||
msgid "Summary:"
|
||||
msgstr ""
|
||||
msgstr "Zusammenfassung:"
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
|
||||
msgid "System Requirements"
|
||||
@ -5089,7 +5089,7 @@ msgstr "Text"
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
|
||||
msgid "Text Align"
|
||||
msgstr ""
|
||||
msgstr "Textausrichtung"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
|
||||
msgid "Text Color"
|
||||
@ -5177,7 +5177,7 @@ msgstr "Die Ereignisse, die einen Webhook auslösen, der an Ihre URL gesendet wi
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:62
|
||||
msgid "The following errors occurred:"
|
||||
msgstr ""
|
||||
msgstr "Die folgenden Fehler sind aufgetreten:"
|
||||
|
||||
#: packages/email/templates/team-delete.tsx:37
|
||||
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
|
||||
@ -5637,7 +5637,7 @@ msgstr "Gesamtempfänger"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:49
|
||||
msgid "Total rows processed: {totalProcessed}"
|
||||
msgstr ""
|
||||
msgstr "Insgesamt verarbeitete Zeilen: {totalProcessed}"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
|
||||
msgid "Total Signers that Signed Up"
|
||||
@ -5899,7 +5899,7 @@ msgstr "Upgrade"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
|
||||
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||
msgstr ""
|
||||
msgstr "Laden Sie eine CSV-Datei hoch, um mehrere Dokumente aus dieser Vorlage zu erstellen. Jede Zeile repräsentiert ein Dokument mit den Empfängerdaten."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
|
||||
msgid "Upload a custom document to use instead of the template's default document"
|
||||
@ -5907,7 +5907,7 @@ msgstr "Laden Sie ein benutzerdefiniertes Dokument hoch, um es anstelle des Stan
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
||||
msgid "Upload and Process"
|
||||
msgstr ""
|
||||
msgstr "Hochladen und verarbeiten"
|
||||
|
||||
#: apps/web/src/components/forms/avatar-image.tsx:179
|
||||
msgid "Upload Avatar"
|
||||
@ -5915,7 +5915,7 @@ msgstr "Avatar hochladen"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
msgstr "CSV hochladen"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
||||
msgid "Upload custom document"
|
||||
@ -6594,7 +6594,7 @@ msgstr "Sie können Dokumente ansehen, die mit dieser E-Mail verknüpft sind, un
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:76
|
||||
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
|
||||
msgstr ""
|
||||
msgstr "Sie können die erstellten Dokumente in Ihrem Dashboard unter der Rubrik \"Dokumente, die aus Vorlage erstellt wurden\" einsehen."
|
||||
|
||||
#: packages/email/template-components/template-document-rejected.tsx:37
|
||||
msgid "You can view the document and its status by clicking the button below."
|
||||
@ -6807,11 +6807,11 @@ msgstr "Ihre Markenpräferenzen wurden aktualisiert"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr ""
|
||||
msgstr "Ihr Massenversand wurde gestartet. Sie erhalten eine E-Mail-Benachrichtigung nach Abschluss."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:40
|
||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||
msgstr ""
|
||||
msgstr "Ihre Massenversandoperation für Vorlage \"{templateName}\" ist abgeschlossen."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
||||
msgid "Your current plan is past due. Please update your payment information."
|
||||
@ -6962,3 +6962,4 @@ msgstr "Ihr Token wurde erfolgreich erstellt! Stellen Sie sicher, dass Sie es ko
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
|
||||
msgid "Your tokens will be shown here once you create them."
|
||||
msgstr "Ihre Tokens werden hier angezeigt, sobald Sie sie erstellt haben."
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-01-30 03:57\n"
|
||||
"PO-Revision-Date: 2025-01-30 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -163,7 +163,7 @@ msgstr "{inviterName} te ha eliminado del documento<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:61
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{inviterName} en nombre de \"{teamName}\" te ha invitado a {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:45
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
|
||||
@ -308,7 +308,7 @@ msgstr "{signerName} ha rechazado el documento \"{documentName}\"."
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:68
|
||||
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{teamName} te ha invitado a {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:46
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
@ -1232,20 +1232,20 @@ msgstr "Importación masiva"
|
||||
|
||||
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
|
||||
msgid "Bulk Send Complete: {0}"
|
||||
msgstr ""
|
||||
msgstr "Envío Masivo Completo: {0}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:30
|
||||
msgid "Bulk send operation complete for template \"{templateName}\""
|
||||
msgstr ""
|
||||
msgstr "Operación de envío masivo completa para la plantilla \"{templateName}\""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
|
||||
msgid "Bulk Send Template via CSV"
|
||||
msgstr ""
|
||||
msgstr "Enviar plantilla masiva a través de CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
|
||||
msgid "Bulk Send via CSV"
|
||||
msgstr ""
|
||||
msgstr "Envío Masivo vía CSV"
|
||||
|
||||
#: packages/email/templates/team-invite.tsx:84
|
||||
msgid "by <0>{senderName}</0>"
|
||||
@ -1338,12 +1338,12 @@ msgstr "No se puede eliminar el firmante"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:18
|
||||
msgid "Cc"
|
||||
msgstr "Cc"
|
||||
msgstr "Copia visible"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:15
|
||||
#: packages/lib/constants/recipient-roles.ts:17
|
||||
msgid "CC"
|
||||
msgstr "CC"
|
||||
msgstr "COPIA VISIBLE"
|
||||
|
||||
#: packages/lib/constants/recipient-roles.ts:16
|
||||
msgid "CC'd"
|
||||
@ -1788,7 +1788,7 @@ msgstr "Creado el {0}"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
|
||||
msgid "CSV Structure"
|
||||
msgstr ""
|
||||
msgstr "Estructura CSV"
|
||||
|
||||
#: apps/web/src/components/forms/password.tsx:112
|
||||
msgid "Current Password"
|
||||
@ -1800,7 +1800,7 @@ msgstr "La contraseña actual es incorrecta."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
|
||||
msgid "Current recipients:"
|
||||
msgstr ""
|
||||
msgstr "Destinatarios actuales:"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
|
||||
msgid "Daily"
|
||||
@ -2328,7 +2328,7 @@ msgstr "Descargar certificado"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
|
||||
msgid "Download Template CSV"
|
||||
msgstr ""
|
||||
msgstr "Descargar Plantilla CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
|
||||
#: apps/web/src/components/formatter/document-status.tsx:34
|
||||
@ -2659,7 +2659,7 @@ msgstr "Falló al actualizar el webhook"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:55
|
||||
msgid "Failed: {failedCount}"
|
||||
msgstr ""
|
||||
msgstr "Fallidos: {failedCount}"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
|
||||
msgid "Field character limit"
|
||||
@ -2721,7 +2721,7 @@ msgstr "Si tiene alguna pregunta sobre esta divulgación, firmas electrónicas o
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
|
||||
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||
msgstr ""
|
||||
msgstr "Para cada destinatario, proporciona su correo electrónico (obligatorio) y nombre (opcional) en columnas separadas. Descarga el modelo CSV a continuación para el formato correcto."
|
||||
|
||||
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
||||
msgid "Forgot Password?"
|
||||
@ -2834,7 +2834,7 @@ msgstr "Hola, soy Timur"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:36
|
||||
msgid "Hi {userName},"
|
||||
msgstr ""
|
||||
msgstr "Hola, {userName},"
|
||||
|
||||
#: packages/email/templates/reset-password.tsx:56
|
||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
||||
@ -3238,7 +3238,7 @@ msgstr "Máx"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
|
||||
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||
msgstr ""
|
||||
msgstr "Tamaño máximo de archivo: 4MB. Máximo 100 filas por carga. Los valores en blanco usarán los valores predeterminados de la plantilla."
|
||||
|
||||
#: packages/lib/constants/teams.ts:12
|
||||
msgid "Member"
|
||||
@ -3868,7 +3868,7 @@ msgstr "Por favor, escribe <0>{0}</0> para confirmar."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr ""
|
||||
msgstr "Plantilla CSV preformateada con datos de ejemplo."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
|
||||
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
|
||||
@ -4392,7 +4392,7 @@ msgstr "Enviar documentos en nombre del equipo usando la dirección de correo el
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
|
||||
msgid "Send documents to recipients immediately"
|
||||
msgstr ""
|
||||
msgstr "Enviar documentos a los destinatarios inmediatamente"
|
||||
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
|
||||
msgid "Send on Behalf of Team"
|
||||
@ -4864,11 +4864,11 @@ msgstr "Clave de acceso creada con éxito"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:52
|
||||
msgid "Successfully created: {successCount}"
|
||||
msgstr ""
|
||||
msgstr "Creado con éxito: {successCount}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:44
|
||||
msgid "Summary:"
|
||||
msgstr ""
|
||||
msgstr "Resumen:"
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
|
||||
msgid "System Requirements"
|
||||
@ -5089,7 +5089,7 @@ msgstr "Texto"
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
|
||||
msgid "Text Align"
|
||||
msgstr ""
|
||||
msgstr "Alineación de texto"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
|
||||
msgid "Text Color"
|
||||
@ -5177,7 +5177,7 @@ msgstr "Los eventos que activarán un webhook para ser enviado a tu URL."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:62
|
||||
msgid "The following errors occurred:"
|
||||
msgstr ""
|
||||
msgstr "Se produjeron los siguientes errores:"
|
||||
|
||||
#: packages/email/templates/team-delete.tsx:37
|
||||
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
|
||||
@ -5637,7 +5637,7 @@ msgstr "Total de destinatarios"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:49
|
||||
msgid "Total rows processed: {totalProcessed}"
|
||||
msgstr ""
|
||||
msgstr "Filas totales procesadas: {totalProcessed}"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
|
||||
msgid "Total Signers that Signed Up"
|
||||
@ -5899,7 +5899,7 @@ msgstr "Actualizar"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
|
||||
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||
msgstr ""
|
||||
msgstr "Sube un archivo CSV para crear múltiples documentos a partir de esta plantilla. Cada fila representa un documento con los detalles del destinatario."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
|
||||
msgid "Upload a custom document to use instead of the template's default document"
|
||||
@ -5907,7 +5907,7 @@ msgstr "Sube un documento personalizado para usar en lugar del documento predete
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
||||
msgid "Upload and Process"
|
||||
msgstr ""
|
||||
msgstr "Subir y procesar"
|
||||
|
||||
#: apps/web/src/components/forms/avatar-image.tsx:179
|
||||
msgid "Upload Avatar"
|
||||
@ -5915,7 +5915,7 @@ msgstr "Subir avatar"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
msgstr "Subir CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
||||
msgid "Upload custom document"
|
||||
@ -6594,7 +6594,7 @@ msgstr "Puedes ver documentos asociados a este correo electrónico y usar esta i
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:76
|
||||
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
|
||||
msgstr ""
|
||||
msgstr "Puedes ver los documentos creados en tu panel de control bajo la sección \"Documentos creados a partir de la plantilla\"."
|
||||
|
||||
#: packages/email/template-components/template-document-rejected.tsx:37
|
||||
msgid "You can view the document and its status by clicking the button below."
|
||||
@ -6807,11 +6807,11 @@ msgstr "Tus preferencias de marca han sido actualizadas"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr ""
|
||||
msgstr "Tu envío masivo ha sido iniciado. Recibirás una notificación por correo electrónico al completarse."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:40
|
||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||
msgstr ""
|
||||
msgstr "Tu operación de envío masivo para la plantilla \"{templateName}\" ha sido completada."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
||||
msgid "Your current plan is past due. Please update your payment information."
|
||||
@ -6962,3 +6962,4 @@ msgstr "¡Tu token se creó con éxito! ¡Asegúrate de copiarlo porque no podr
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
|
||||
msgid "Your tokens will be shown here once you create them."
|
||||
msgstr "Tus tokens se mostrarán aquí una vez que los crees."
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-01-30 03:57\n"
|
||||
"PO-Revision-Date: 2025-01-30 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
@ -163,7 +163,7 @@ msgstr "{inviterName} vous a retiré du document<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:61
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{inviterName} représentant \"{teamName}\" vous a invité à {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:45
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
|
||||
@ -308,7 +308,7 @@ msgstr "{signerName} a rejeté le document \"{documentName}\"."
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:68
|
||||
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{teamName} vous a invité à {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:46
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
@ -1232,20 +1232,20 @@ msgstr "Importation en masse"
|
||||
|
||||
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
|
||||
msgid "Bulk Send Complete: {0}"
|
||||
msgstr ""
|
||||
msgstr "Envoi en masse terminé : {0}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:30
|
||||
msgid "Bulk send operation complete for template \"{templateName}\""
|
||||
msgstr ""
|
||||
msgstr "Envoi groupé terminé pour le modèle \"{templateName}\""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
|
||||
msgid "Bulk Send Template via CSV"
|
||||
msgstr ""
|
||||
msgstr "Envoi de modèle groupé via CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
|
||||
msgid "Bulk Send via CSV"
|
||||
msgstr ""
|
||||
msgstr "Envoi en masse via CSV"
|
||||
|
||||
#: packages/email/templates/team-invite.tsx:84
|
||||
msgid "by <0>{senderName}</0>"
|
||||
@ -1788,7 +1788,7 @@ msgstr "Créé le {0}"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
|
||||
msgid "CSV Structure"
|
||||
msgstr ""
|
||||
msgstr "Structure CSV"
|
||||
|
||||
#: apps/web/src/components/forms/password.tsx:112
|
||||
msgid "Current Password"
|
||||
@ -1800,7 +1800,7 @@ msgstr "Le mot de passe actuel est incorrect."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
|
||||
msgid "Current recipients:"
|
||||
msgstr ""
|
||||
msgstr "Destinataires actuels :"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
|
||||
msgid "Daily"
|
||||
@ -2328,7 +2328,7 @@ msgstr "Télécharger le certificat"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
|
||||
msgid "Download Template CSV"
|
||||
msgstr ""
|
||||
msgstr "Télécharger le modèle CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
|
||||
#: apps/web/src/components/formatter/document-status.tsx:34
|
||||
@ -2659,7 +2659,7 @@ msgstr "Échec de la mise à jour du webhook"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:55
|
||||
msgid "Failed: {failedCount}"
|
||||
msgstr ""
|
||||
msgstr "Échoués : {failedCount}"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
|
||||
msgid "Field character limit"
|
||||
@ -2721,7 +2721,7 @@ msgstr "Pour toute question concernant cette divulgation, les signatures électr
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
|
||||
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||
msgstr ""
|
||||
msgstr "Pour chaque destinataire, fournissez leur email (obligatoire) et leur nom (facultatif) dans des colonnes séparées. Téléchargez le modèle CSV ci-dessous pour le format correct."
|
||||
|
||||
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
||||
msgid "Forgot Password?"
|
||||
@ -2834,7 +2834,7 @@ msgstr "Salut, je suis Timur"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:36
|
||||
msgid "Hi {userName},"
|
||||
msgstr ""
|
||||
msgstr "Bonjour, {userName},"
|
||||
|
||||
#: packages/email/templates/reset-password.tsx:56
|
||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
||||
@ -3238,7 +3238,7 @@ msgstr ""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
|
||||
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||
msgstr ""
|
||||
msgstr "Taille maximale du fichier : 4 Mo. Maximum de 100 lignes par téléversement. Les valeurs vides utiliseront les valeurs par défaut du modèle."
|
||||
|
||||
#: packages/lib/constants/teams.ts:12
|
||||
msgid "Member"
|
||||
@ -3868,7 +3868,7 @@ msgstr "Veuillez taper <0>{0}</0> pour confirmer."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr ""
|
||||
msgstr "Modèle CSV pré-formaté avec des données d'exemple."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
|
||||
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
|
||||
@ -4392,7 +4392,7 @@ msgstr "Envoyer des documents au nom de l'équipe en utilisant l'adresse e-mail"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
|
||||
msgid "Send documents to recipients immediately"
|
||||
msgstr ""
|
||||
msgstr "Envoyer les documents aux destinataires immédiatement"
|
||||
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
|
||||
msgid "Send on Behalf of Team"
|
||||
@ -4864,11 +4864,11 @@ msgstr "Clé d'authentification créée avec succès"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:52
|
||||
msgid "Successfully created: {successCount}"
|
||||
msgstr ""
|
||||
msgstr "Créés avec succès : {successCount}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:44
|
||||
msgid "Summary:"
|
||||
msgstr ""
|
||||
msgstr "Résumé :"
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
|
||||
msgid "System Requirements"
|
||||
@ -5089,7 +5089,7 @@ msgstr "Texte"
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
|
||||
msgid "Text Align"
|
||||
msgstr ""
|
||||
msgstr "Alignement du texte"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
|
||||
msgid "Text Color"
|
||||
@ -5177,7 +5177,7 @@ msgstr "Les événements qui déclencheront un webhook à envoyer à votre URL."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:62
|
||||
msgid "The following errors occurred:"
|
||||
msgstr ""
|
||||
msgstr "Les erreurs suivantes se sont produites :"
|
||||
|
||||
#: packages/email/templates/team-delete.tsx:37
|
||||
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
|
||||
@ -5637,7 +5637,7 @@ msgstr "Total des destinataires"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:49
|
||||
msgid "Total rows processed: {totalProcessed}"
|
||||
msgstr ""
|
||||
msgstr "Lignes totales traitées : {totalProcessed}"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
|
||||
msgid "Total Signers that Signed Up"
|
||||
@ -5899,7 +5899,7 @@ msgstr "Améliorer"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
|
||||
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||
msgstr ""
|
||||
msgstr "Téléchargez un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec ses détails de destinataire."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
|
||||
msgid "Upload a custom document to use instead of the template's default document"
|
||||
@ -5907,7 +5907,7 @@ msgstr "Téléchargez un document personnalisé à utiliser à la place du docum
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
||||
msgid "Upload and Process"
|
||||
msgstr ""
|
||||
msgstr "Télécharger et traiter"
|
||||
|
||||
#: apps/web/src/components/forms/avatar-image.tsx:179
|
||||
msgid "Upload Avatar"
|
||||
@ -5915,7 +5915,7 @@ msgstr "Télécharger un avatar"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
msgstr "Télécharger le CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
||||
msgid "Upload custom document"
|
||||
@ -6594,7 +6594,7 @@ msgstr "Vous pouvez voir les documents associés à cet e-mail et utiliser cette
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:76
|
||||
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
|
||||
msgstr ""
|
||||
msgstr "Vous pouvez voir les documents créés dans votre tableau de bord sous la section \"Documents créés à partir du modèle\"."
|
||||
|
||||
#: packages/email/template-components/template-document-rejected.tsx:37
|
||||
msgid "You can view the document and its status by clicking the button below."
|
||||
@ -6807,11 +6807,11 @@ msgstr "Vos préférences de branding ont été mises à jour"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr ""
|
||||
msgstr "Votre envoi groupé a été initié. Vous recevrez une notification par email une fois terminé."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:40
|
||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||
msgstr ""
|
||||
msgstr "Votre opération d'envoi groupé pour le modèle \"{templateName}\" est terminée."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
||||
msgid "Your current plan is past due. Please update your payment information."
|
||||
@ -6962,3 +6962,4 @@ msgstr "Votre token a été créé avec succès ! Assurez-vous de le copier car
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
|
||||
msgid "Your tokens will be shown here once you create them."
|
||||
msgstr "Vos tokens seront affichés ici une fois que vous les aurez créés."
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-01-30 03:57\n"
|
||||
"PO-Revision-Date: 2025-01-30 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@ -163,7 +163,7 @@ msgstr "{inviterName} ti ha rimosso dal documento<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:61
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{inviterName} per conto di \"{teamName}\" ti ha invitato a {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:45
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
|
||||
@ -308,7 +308,7 @@ msgstr "{signerName} ha rifiutato il documento \"{documentName}\"."
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:68
|
||||
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{teamName} ti ha invitato a {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:46
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
@ -1232,20 +1232,20 @@ msgstr "Importazione Massiva"
|
||||
|
||||
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
|
||||
msgid "Bulk Send Complete: {0}"
|
||||
msgstr ""
|
||||
msgstr "Invio Massivo Completato: {0}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:30
|
||||
msgid "Bulk send operation complete for template \"{templateName}\""
|
||||
msgstr ""
|
||||
msgstr "Operazione di invio massivo completata per il modello \"{templateName}\""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
|
||||
msgid "Bulk Send Template via CSV"
|
||||
msgstr ""
|
||||
msgstr "Invio modello in blocco tramite CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
|
||||
msgid "Bulk Send via CSV"
|
||||
msgstr ""
|
||||
msgstr "Invio Massivo via CSV"
|
||||
|
||||
#: packages/email/templates/team-invite.tsx:84
|
||||
msgid "by <0>{senderName}</0>"
|
||||
@ -1788,7 +1788,7 @@ msgstr "Creato il {0}"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
|
||||
msgid "CSV Structure"
|
||||
msgstr ""
|
||||
msgstr "Struttura CSV"
|
||||
|
||||
#: apps/web/src/components/forms/password.tsx:112
|
||||
msgid "Current Password"
|
||||
@ -1800,7 +1800,7 @@ msgstr "La password corrente è errata."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
|
||||
msgid "Current recipients:"
|
||||
msgstr ""
|
||||
msgstr "Destinatari attuali:"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
|
||||
msgid "Daily"
|
||||
@ -2328,7 +2328,7 @@ msgstr "Scarica il certificato"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
|
||||
msgid "Download Template CSV"
|
||||
msgstr ""
|
||||
msgstr "Scarica Modello CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
|
||||
#: apps/web/src/components/formatter/document-status.tsx:34
|
||||
@ -2659,7 +2659,7 @@ msgstr "Aggiornamento webhook fallito"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:55
|
||||
msgid "Failed: {failedCount}"
|
||||
msgstr ""
|
||||
msgstr "Falliti: {failedCount}"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
|
||||
msgid "Field character limit"
|
||||
@ -2721,7 +2721,7 @@ msgstr "Per qualsiasi domanda riguardante questa divulgazione, le firme elettron
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
|
||||
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||
msgstr ""
|
||||
msgstr "Per ogni destinatario, fornisci la loro email (obbligatoria) e il nome (opzionale) in colonne separate. Scarica il modello CSV qui sotto per il formato corretto."
|
||||
|
||||
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
||||
msgid "Forgot Password?"
|
||||
@ -2834,7 +2834,7 @@ msgstr "Ciao, sono Timur"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:36
|
||||
msgid "Hi {userName},"
|
||||
msgstr ""
|
||||
msgstr "Ciao {userName},"
|
||||
|
||||
#: packages/email/templates/reset-password.tsx:56
|
||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
||||
@ -3238,7 +3238,7 @@ msgstr ""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
|
||||
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||
msgstr ""
|
||||
msgstr "Dimensione massima del file: 4MB. Massimo 100 righe per caricamento. I valori vuoti utilizzeranno i valori predefiniti del modello."
|
||||
|
||||
#: packages/lib/constants/teams.ts:12
|
||||
msgid "Member"
|
||||
@ -3868,7 +3868,7 @@ msgstr "Si prega di digitare <0>{0}</0> per confermare."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr ""
|
||||
msgstr "Modello CSV preformattato con dati di esempio."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
|
||||
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
|
||||
@ -4392,7 +4392,7 @@ msgstr "Invia documenti a nome del team utilizzando l'indirizzo email"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
|
||||
msgid "Send documents to recipients immediately"
|
||||
msgstr ""
|
||||
msgstr "Invia documenti ai destinatari immediatamente"
|
||||
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
|
||||
msgid "Send on Behalf of Team"
|
||||
@ -4864,11 +4864,11 @@ msgstr "Chiave di accesso creata con successo"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:52
|
||||
msgid "Successfully created: {successCount}"
|
||||
msgstr ""
|
||||
msgstr "Creati con successo: {successCount}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:44
|
||||
msgid "Summary:"
|
||||
msgstr ""
|
||||
msgstr "Sommario:"
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
|
||||
msgid "System Requirements"
|
||||
@ -5089,7 +5089,7 @@ msgstr "Testo"
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
|
||||
msgid "Text Align"
|
||||
msgstr ""
|
||||
msgstr "Allineamento del testo"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
|
||||
msgid "Text Color"
|
||||
@ -5177,7 +5177,7 @@ msgstr "Gli eventi che scateneranno un webhook da inviare al tuo URL."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:62
|
||||
msgid "The following errors occurred:"
|
||||
msgstr ""
|
||||
msgstr "Si sono verificati i seguenti errori:"
|
||||
|
||||
#: packages/email/templates/team-delete.tsx:37
|
||||
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
|
||||
@ -5637,7 +5637,7 @@ msgstr "Totale destinatari"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:49
|
||||
msgid "Total rows processed: {totalProcessed}"
|
||||
msgstr ""
|
||||
msgstr "Righe totali elaborate: {totalProcessed}"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
|
||||
msgid "Total Signers that Signed Up"
|
||||
@ -5899,7 +5899,7 @@ msgstr "Aggiorna"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
|
||||
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||
msgstr ""
|
||||
msgstr "Carica un file CSV per creare più documenti da questo modello. Ogni riga rappresenta un documento con i dettagli del destinatario."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
|
||||
msgid "Upload a custom document to use instead of the template's default document"
|
||||
@ -5907,7 +5907,7 @@ msgstr "Carica un documento personalizzato da utilizzare al posto del documento
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
||||
msgid "Upload and Process"
|
||||
msgstr ""
|
||||
msgstr "Carica e elabora"
|
||||
|
||||
#: apps/web/src/components/forms/avatar-image.tsx:179
|
||||
msgid "Upload Avatar"
|
||||
@ -5915,7 +5915,7 @@ msgstr "Carica Avatar"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
msgstr "Carica CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
||||
msgid "Upload custom document"
|
||||
@ -6594,7 +6594,7 @@ msgstr "Puoi visualizzare i documenti associati a questa email e utilizzare ques
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:76
|
||||
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
|
||||
msgstr ""
|
||||
msgstr "Puoi visualizzare i documenti creati nel tuo dashboard nella sezione \"Documenti creati dal modello\"."
|
||||
|
||||
#: packages/email/template-components/template-document-rejected.tsx:37
|
||||
msgid "You can view the document and its status by clicking the button below."
|
||||
@ -6807,11 +6807,11 @@ msgstr "Le tue preferenze di branding sono state aggiornate"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr ""
|
||||
msgstr "Il tuo invio massivo è stato avviato. Riceverai una notifica via email al completamento."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:40
|
||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||
msgstr ""
|
||||
msgstr "La tua operazione di invio massivo per il modello \"{templateName}\" è stata completata."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
||||
msgid "Your current plan is past due. Please update your payment information."
|
||||
@ -6962,3 +6962,4 @@ msgstr "Il tuo token è stato creato con successo! Assicurati di copiarlo perch
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
|
||||
msgid "Your tokens will be shown here once you create them."
|
||||
msgstr "I tuoi token verranno mostrati qui una volta creati."
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pl\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-01-24 12:04\n"
|
||||
"PO-Revision-Date: 2025-01-30 06:04\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
@ -163,7 +163,7 @@ msgstr "{inviterName} usunął cię z dokumentu<0/>„{documentName}”"
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:61
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{inviterName} w imieniu \"{teamName}\" zaprosił Cię do {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:45
|
||||
msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}"
|
||||
@ -308,7 +308,7 @@ msgstr "{signerName} odrzucił dokument \"{documentName}\"."
|
||||
|
||||
#: packages/email/template-components/template-document-invite.tsx:68
|
||||
msgid "{teamName} has invited you to {0}<0/>\"{documentName}\""
|
||||
msgstr ""
|
||||
msgstr "{teamName} zaprosił Cię do {0}<0/>\"{documentName}\""
|
||||
|
||||
#: packages/email/templates/document-invite.tsx:46
|
||||
msgid "{teamName} has invited you to {action} {documentName}"
|
||||
@ -1232,20 +1232,20 @@ msgstr "Import zbiorczy"
|
||||
|
||||
#: packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts:203
|
||||
msgid "Bulk Send Complete: {0}"
|
||||
msgstr ""
|
||||
msgstr "Zakończono wysyłkę zbiorczą: {0}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:30
|
||||
msgid "Bulk send operation complete for template \"{templateName}\""
|
||||
msgstr ""
|
||||
msgstr "Zakończono operację masowej wysyłki dla szablonu \"{templateName}\""
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:128
|
||||
msgid "Bulk Send Template via CSV"
|
||||
msgstr ""
|
||||
msgstr "Szablon masowej wysyłki przez CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx:97
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:120
|
||||
msgid "Bulk Send via CSV"
|
||||
msgstr ""
|
||||
msgstr "Zbiorcza wysyłka przez CSV"
|
||||
|
||||
#: packages/email/templates/team-invite.tsx:84
|
||||
msgid "by <0>{senderName}</0>"
|
||||
@ -1788,7 +1788,7 @@ msgstr "Utworzono {0}"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:143
|
||||
msgid "CSV Structure"
|
||||
msgstr ""
|
||||
msgstr "Struktura CSV"
|
||||
|
||||
#: apps/web/src/components/forms/password.tsx:112
|
||||
msgid "Current Password"
|
||||
@ -1800,7 +1800,7 @@ msgstr "Aktualne hasło jest niepoprawne."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:154
|
||||
msgid "Current recipients:"
|
||||
msgstr ""
|
||||
msgstr "Aktualni odbiorcy:"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx:28
|
||||
msgid "Daily"
|
||||
@ -2328,7 +2328,7 @@ msgstr "Pobierz certyfikat"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:168
|
||||
msgid "Download Template CSV"
|
||||
msgstr ""
|
||||
msgstr "Pobierz szablon CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/[id]/template-page-view-documents-table.tsx:208
|
||||
#: apps/web/src/components/formatter/document-status.tsx:34
|
||||
@ -2659,7 +2659,7 @@ msgstr "Nie udało się zaktualizować webhooku"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:55
|
||||
msgid "Failed: {failedCount}"
|
||||
msgstr ""
|
||||
msgstr "Niepowodzenia: {failedCount}"
|
||||
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:100
|
||||
msgid "Field character limit"
|
||||
@ -2721,7 +2721,7 @@ msgstr "W przypadku jakichkolwiek pytań dotyczących tego ujawnienia, podpisów
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:147
|
||||
msgid "For each recipient, provide their email (required) and name (optional) in separate columns. Download the template CSV below for the correct format."
|
||||
msgstr ""
|
||||
msgstr "Dla każdego odbiorcy podaj jego email (wymagany) i nazwę (opcjonalnie) w oddzielnych kolumnach. Pobierz poniżej szablon CSV dla właściwego formatu."
|
||||
|
||||
#: packages/lib/server-only/auth/send-forgot-password.ts:61
|
||||
msgid "Forgot Password?"
|
||||
@ -2834,7 +2834,7 @@ msgstr "Cześć, jestem Timur"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:36
|
||||
msgid "Hi {userName},"
|
||||
msgstr ""
|
||||
msgstr "Cześć, {userName},"
|
||||
|
||||
#: packages/email/templates/reset-password.tsx:56
|
||||
msgid "Hi, {userName} <0>({userEmail})</0>"
|
||||
@ -3238,7 +3238,7 @@ msgstr "Max"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:227
|
||||
msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults."
|
||||
msgstr ""
|
||||
msgstr "Maksymalny rozmiar pliku: 4MB. Maksymalnie 100 wierszy na przesyłkę. Puste wartości zostaną zastąpione domyślnymi z szablonu."
|
||||
|
||||
#: packages/lib/constants/teams.ts:12
|
||||
msgid "Member"
|
||||
@ -3868,7 +3868,7 @@ msgstr "Wpisz <0>{0}</0>, aby potwierdzić."
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:172
|
||||
msgid "Pre-formatted CSV template with example data."
|
||||
msgstr ""
|
||||
msgstr "Wstępnie sformatowany szablon CSV z przykładowymi danymi."
|
||||
|
||||
#: apps/web/src/components/(dashboard)/common/command-menu.tsx:214
|
||||
#: apps/web/src/components/(teams)/settings/layout/desktop-nav.tsx:58
|
||||
@ -4392,7 +4392,7 @@ msgstr "Wyślij dokumenty w imieniu zespołu, używając adresu e-mail"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:253
|
||||
msgid "Send documents to recipients immediately"
|
||||
msgstr ""
|
||||
msgstr "Wyślij dokumenty do odbiorców natychmiast"
|
||||
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/preferences/document-preferences.tsx:200
|
||||
msgid "Send on Behalf of Team"
|
||||
@ -4864,11 +4864,11 @@ msgstr "Pomyślnie utworzono klucz uwierzytelniający"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:52
|
||||
msgid "Successfully created: {successCount}"
|
||||
msgstr ""
|
||||
msgstr "Pomyślnie utworzono: {successCount}"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:44
|
||||
msgid "Summary:"
|
||||
msgstr ""
|
||||
msgstr "Podsumowanie:"
|
||||
|
||||
#: apps/web/src/app/(unauthenticated)/articles/signature-disclosure/page.tsx:57
|
||||
msgid "System Requirements"
|
||||
@ -5089,7 +5089,7 @@ msgstr "Tekst"
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx:140
|
||||
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx:124
|
||||
msgid "Text Align"
|
||||
msgstr ""
|
||||
msgstr "Wyrównanie tekstu"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/site-settings/banner-form.tsx:157
|
||||
msgid "Text Color"
|
||||
@ -5177,7 +5177,7 @@ msgstr "Wydarzenia, które wyzwolą webhook do wysłania do Twojego URL."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:62
|
||||
msgid "The following errors occurred:"
|
||||
msgstr ""
|
||||
msgstr "Wystąpiły następujące błędy:"
|
||||
|
||||
#: packages/email/templates/team-delete.tsx:37
|
||||
msgid "The following team has been deleted by its owner. You will no longer be able to access this team and its documents"
|
||||
@ -5637,7 +5637,7 @@ msgstr "Łączna liczba odbiorców"
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:49
|
||||
msgid "Total rows processed: {totalProcessed}"
|
||||
msgstr ""
|
||||
msgstr "Łączna liczba przetworzonych wierszy: {totalProcessed}"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/admin/stats/page.tsx:150
|
||||
msgid "Total Signers that Signed Up"
|
||||
@ -5899,7 +5899,7 @@ msgstr "Ulepsz"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:132
|
||||
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
|
||||
msgstr ""
|
||||
msgstr "Prześlij plik CSV, aby utworzyć wiele dokumentów z tego szablonu. Każda linia reprezentuje jeden dokument z jego szczegółami odbiorcy."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:426
|
||||
msgid "Upload a custom document to use instead of the template's default document"
|
||||
@ -5907,7 +5907,7 @@ msgstr "Prześlij niestandardowy dokument do użycia zamiast domyślnego dokumen
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:267
|
||||
msgid "Upload and Process"
|
||||
msgstr ""
|
||||
msgstr "Prześlij i przetwórz"
|
||||
|
||||
#: apps/web/src/components/forms/avatar-image.tsx:179
|
||||
msgid "Upload Avatar"
|
||||
@ -5915,7 +5915,7 @@ msgstr "Prześlij awatar"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:198
|
||||
msgid "Upload CSV"
|
||||
msgstr ""
|
||||
msgstr "Prześlij CSV"
|
||||
|
||||
#: apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx:419
|
||||
msgid "Upload custom document"
|
||||
@ -6594,7 +6594,7 @@ msgstr "Możesz wyświetlać dokumenty powiązane z tym e-mailem i używać tej
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:76
|
||||
msgid "You can view the created documents in your dashboard under the \"Documents created from template\" section."
|
||||
msgstr ""
|
||||
msgstr "Możesz zobaczyć utworzone dokumenty na swoim pulpicie w sekcji \"Dokumenty utworzone z szablonu\"."
|
||||
|
||||
#: packages/email/template-components/template-document-rejected.tsx:37
|
||||
msgid "You can view the document and its status by clicking the button below."
|
||||
@ -6807,11 +6807,11 @@ msgstr "Preferencje dotyczące marki zostały zaktualizowane"
|
||||
|
||||
#: apps/web/src/components/templates/template-bulk-send-dialog.tsx:97
|
||||
msgid "Your bulk send has been initiated. You will receive an email notification upon completion."
|
||||
msgstr ""
|
||||
msgstr "Twoja masowa wysyłka została zainicjowana. Otrzymasz powiadomienie e-mail po jej zakończeniu."
|
||||
|
||||
#: packages/email/templates/bulk-send-complete.tsx:40
|
||||
msgid "Your bulk send operation for template \"{templateName}\" has completed."
|
||||
msgstr ""
|
||||
msgstr "Twoja operacja masowej wysyłki dla szablonu \"{templateName}\" została zakończona."
|
||||
|
||||
#: apps/web/src/app/(dashboard)/settings/billing/page.tsx:125
|
||||
msgid "Your current plan is past due. Please update your payment information."
|
||||
@ -6962,3 +6962,4 @@ msgstr "Twój token został pomyślnie utworzony! Upewnij się, że go skopiujes
|
||||
#: apps/web/src/app/(teams)/t/[teamUrl]/settings/tokens/page.tsx:86
|
||||
msgid "Your tokens will be shown here once you create them."
|
||||
msgstr "Twoje tokeny będą tutaj wyświetlane po ich utworzeniu."
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'DOCUMENT_DELETED', // When the document is soft deleted.
|
||||
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
||||
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
||||
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
|
||||
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
|
||||
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
||||
@ -45,6 +46,7 @@ export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
||||
'SIGNING_REQUEST',
|
||||
'VIEW_REQUEST',
|
||||
'APPROVE_REQUEST',
|
||||
'ASSISTING_REQUEST',
|
||||
'CC',
|
||||
'DOCUMENT_COMPLETED',
|
||||
]);
|
||||
@ -313,6 +315,83 @@ export const ZDocumentAuditLogEventDocumentFieldUninsertedSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document field prefilled by assistant.
|
||||
*/
|
||||
export const ZDocumentAuditLogEventDocumentFieldPrefilledSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED),
|
||||
data: ZBaseRecipientDataSchema.extend({
|
||||
fieldId: z.string(),
|
||||
|
||||
// Organised into union to allow us to extend each field if required.
|
||||
field: z.union([
|
||||
z.object({
|
||||
type: z.literal(FieldType.INITIALS),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.EMAIL),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.DATE),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NAME),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.TEXT),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.union([z.literal(FieldType.SIGNATURE), z.literal(FieldType.FREE_SIGNATURE)]),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.RADIO),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.CHECKBOX),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.DROPDOWN),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.NUMBER),
|
||||
data: z.string(),
|
||||
}),
|
||||
]),
|
||||
fieldSecurity: z.preprocess(
|
||||
(input) => {
|
||||
const legacyNoneSecurityType = JSON.stringify({
|
||||
type: 'NONE',
|
||||
});
|
||||
|
||||
// Replace legacy 'NONE' field security type with undefined.
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
input !== null &&
|
||||
JSON.stringify(input) === legacyNoneSecurityType
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return input;
|
||||
},
|
||||
z
|
||||
.object({
|
||||
type: ZRecipientActionAuthTypesSchema,
|
||||
})
|
||||
.optional(),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED),
|
||||
data: ZGenericFromToSchema,
|
||||
@ -493,6 +572,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
||||
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
||||
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
||||
|
||||
@ -314,6 +314,10 @@ export const formatDocumentAuditLogAction = (
|
||||
anonymous: msg`Field unsigned`,
|
||||
identified: msg`${prefix} unsigned a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
|
||||
anonymous: msg`Field prefilled by assistant`,
|
||||
identified: msg`${prefix} prefilled a field`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
||||
anonymous: msg`Document visibility updated`,
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "RecipientRole" ADD VALUE 'ASSISTANT';
|
||||
@ -417,6 +417,7 @@ enum RecipientRole {
|
||||
SIGNER
|
||||
VIEWER
|
||||
APPROVER
|
||||
ASSISTANT
|
||||
}
|
||||
|
||||
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
|
||||
|
||||
5
packages/prisma/types/recipient-with-fields.ts
Normal file
5
packages/prisma/types/recipient-with-fields.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
|
||||
export type RecipientWithFields = Recipient & {
|
||||
fields: Field[];
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
@ -11,12 +11,15 @@ import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export type RecipientRoleSelectProps = SelectProps & {
|
||||
hideCCRecipients?: boolean;
|
||||
isAssistantEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>(
|
||||
({ hideCCRecipients, ...props }, ref) => (
|
||||
({ hideCCRecipients, isAssistantEnabled = true, ...props }, ref) => (
|
||||
<Select {...props}>
|
||||
<SelectTrigger ref={ref} className="bg-background w-[50px] p-2">
|
||||
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
|
||||
@ -110,6 +113,42 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
<SelectItem
|
||||
value={RecipientRole.ASSISTANT}
|
||||
disabled={!isAssistantEnabled}
|
||||
className={cn(
|
||||
!isAssistantEnabled &&
|
||||
'cursor-not-allowed opacity-50 data-[disabled]:pointer-events-auto',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-[150px] items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.ASSISTANT]}</span>
|
||||
<Trans>Can prepare</Trans>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||
<p>
|
||||
{isAssistantEnabled ? (
|
||||
<Trans>
|
||||
The recipient can prepare the document for later signers by pre-filling
|
||||
suggest values.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Assistant role is only available when the document is in sequential signing
|
||||
mode.
|
||||
</Trans>
|
||||
)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
|
||||
@ -508,7 +508,15 @@ export const AddFieldsFormPartial = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
||||
const recipientsByRoleToDisplay = recipients.filter(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||
);
|
||||
|
||||
setSelectedSigner(
|
||||
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
|
||||
recipientsByRoleToDisplay[0],
|
||||
);
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
@ -517,6 +525,7 @@ export const AddFieldsFormPartial = ({
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
APPROVER: [],
|
||||
ASSISTANT: [],
|
||||
};
|
||||
|
||||
recipients.forEach((recipient) => {
|
||||
@ -529,7 +538,12 @@ export const AddFieldsFormPartial = ({
|
||||
const recipientsByRoleToDisplay = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
|
||||
.filter(([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER)
|
||||
.filter(
|
||||
([role]) =>
|
||||
role !== RecipientRole.CC &&
|
||||
role !== RecipientRole.VIEWER &&
|
||||
role !== RecipientRole.ASSISTANT,
|
||||
)
|
||||
.map(
|
||||
([role, roleRecipients]) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
@ -544,12 +558,6 @@ export const AddFieldsFormPartial = ({
|
||||
);
|
||||
}, [recipientsByRole]);
|
||||
|
||||
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
|
||||
|
||||
const handleTypedSignatureChange = (value: boolean) => {
|
||||
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
|
||||
};
|
||||
|
||||
const handleAdvancedSettings = () => {
|
||||
setShowAdvancedSettings((prev) => !prev);
|
||||
};
|
||||
@ -687,9 +695,7 @@ export const AddFieldsFormPartial = ({
|
||||
)}
|
||||
|
||||
{!selectedSigner?.email && (
|
||||
<span className="gradie flex-1 truncate text-left">
|
||||
{selectedSigner?.email}
|
||||
</span>
|
||||
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
|
||||
@ -41,6 +41,7 @@ import {
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSignersFormProps = {
|
||||
@ -123,6 +124,7 @@ export const AddSignersFormPartial = ({
|
||||
}, [recipients, form]);
|
||||
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
|
||||
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
|
||||
|
||||
const {
|
||||
setValue,
|
||||
@ -134,6 +136,10 @@ export const AddSignersFormPartial = ({
|
||||
const watchedSigners = watch('signers');
|
||||
const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL;
|
||||
|
||||
const hasAssistantRole = useMemo(() => {
|
||||
return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT);
|
||||
}, [watchedSigners]);
|
||||
|
||||
const normalizeSigningOrders = (signers: typeof watchedSigners) => {
|
||||
return signers
|
||||
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0))
|
||||
@ -233,6 +239,7 @@ export const AddSignersFormPartial = ({
|
||||
const items = Array.from(watchedSigners);
|
||||
const [reorderedSigner] = items.splice(result.source.index, 1);
|
||||
|
||||
// Find next valid position
|
||||
let insertIndex = result.destination.index;
|
||||
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
|
||||
insertIndex++;
|
||||
@ -240,126 +247,116 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
items.splice(insertIndex, 0, reorderedSigner);
|
||||
|
||||
const updatedSigners = items.map((item, index) => ({
|
||||
...item,
|
||||
signingOrder: !canRecipientBeModified(item.nativeId) ? item.signingOrder : index + 1,
|
||||
const updatedSigners = items.map((signer, index) => ({
|
||||
...signer,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : index + 1,
|
||||
}));
|
||||
|
||||
updatedSigners.forEach((item, index) => {
|
||||
const keys: (keyof typeof item)[] = [
|
||||
'formId',
|
||||
'nativeId',
|
||||
'email',
|
||||
'name',
|
||||
'role',
|
||||
'signingOrder',
|
||||
'actionAuth',
|
||||
];
|
||||
keys.forEach((key) => {
|
||||
form.setValue(`signers.${index}.${key}` as const, item[key]);
|
||||
});
|
||||
});
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
const currentLength = form.getValues('signers').length;
|
||||
if (currentLength > updatedSigners.length) {
|
||||
for (let i = updatedSigners.length; i < currentLength; i++) {
|
||||
form.unregister(`signers.${i}`);
|
||||
}
|
||||
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
await form.trigger('signers');
|
||||
},
|
||||
[form, canRecipientBeModified, watchedSigners],
|
||||
[form, canRecipientBeModified, watchedSigners, toast],
|
||||
);
|
||||
|
||||
const triggerDragAndDrop = useCallback(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
if (!$sensorApi.current) {
|
||||
const handleRoleChange = useCallback(
|
||||
(index: number, role: RecipientRole) => {
|
||||
const currentSigners = form.getValues('signers');
|
||||
const signingOrder = form.getValues('signingOrder');
|
||||
|
||||
// Handle parallel to sequential conversion for assistants
|
||||
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||
toast({
|
||||
title: _(msg`Signing order is enabled.`),
|
||||
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const draggableId = signers[fromIndex].id;
|
||||
const updatedSigners = currentSigners.map((signer, idx) => ({
|
||||
...signer,
|
||||
role: idx === index ? role : signer.role,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : idx + 1,
|
||||
}));
|
||||
|
||||
const preDrag = $sensorApi.current.tryGetLock(draggableId);
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
if (!preDrag) {
|
||||
return;
|
||||
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const drag = preDrag.snapLift();
|
||||
|
||||
setTimeout(() => {
|
||||
// Move directly to the target index
|
||||
if (fromIndex < toIndex) {
|
||||
for (let i = fromIndex; i < toIndex; i++) {
|
||||
drag.moveDown();
|
||||
}
|
||||
} else {
|
||||
for (let i = fromIndex; i > toIndex; i--) {
|
||||
drag.moveUp();
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
drag.drop();
|
||||
}, 500);
|
||||
}, 0);
|
||||
},
|
||||
[signers],
|
||||
);
|
||||
|
||||
const updateSigningOrders = useCallback(
|
||||
(newIndex: number, oldIndex: number) => {
|
||||
const updatedSigners = form.getValues('signers').map((signer, index) => {
|
||||
if (index === oldIndex) {
|
||||
return { ...signer, signingOrder: newIndex + 1 };
|
||||
} else if (index >= newIndex && index < oldIndex) {
|
||||
return {
|
||||
...signer,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId)
|
||||
? signer.signingOrder
|
||||
: (signer.signingOrder ?? index + 1) + 1,
|
||||
};
|
||||
} else if (index <= newIndex && index > oldIndex) {
|
||||
return {
|
||||
...signer,
|
||||
signingOrder: !canRecipientBeModified(signer.nativeId)
|
||||
? signer.signingOrder
|
||||
: Math.max(1, (signer.signingOrder ?? index + 1) - 1),
|
||||
};
|
||||
}
|
||||
return signer;
|
||||
});
|
||||
|
||||
updatedSigners.forEach((signer, index) => {
|
||||
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
|
||||
});
|
||||
},
|
||||
[form, canRecipientBeModified],
|
||||
[form, toast, canRecipientBeModified],
|
||||
);
|
||||
|
||||
const handleSigningOrderChange = useCallback(
|
||||
(index: number, newOrderString: string) => {
|
||||
const newOrder = parseInt(newOrderString, 10);
|
||||
|
||||
if (!newOrderString.trim()) {
|
||||
const trimmedOrderString = newOrderString.trim();
|
||||
if (!trimmedOrderString) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newOrder)) {
|
||||
form.setValue(`signers.${index}.signingOrder`, index + 1);
|
||||
const newOrder = Number(trimmedOrderString);
|
||||
if (!Number.isInteger(newOrder) || newOrder < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = newOrder - 1;
|
||||
if (index !== newIndex) {
|
||||
updateSigningOrders(newIndex, index);
|
||||
triggerDragAndDrop(index, newIndex);
|
||||
const currentSigners = form.getValues('signers');
|
||||
const signer = currentSigners[index];
|
||||
|
||||
// Remove signer from current position and insert at new position
|
||||
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
|
||||
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
|
||||
remainingSigners.splice(newPosition, 0, signer);
|
||||
|
||||
const updatedSigners = remainingSigners.map((s, idx) => ({
|
||||
...s,
|
||||
signingOrder: !canRecipientBeModified(s.nativeId) ? s.signingOrder : idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[form, triggerDragAndDrop, updateSigningOrders],
|
||||
[form, canRecipientBeModified, toast],
|
||||
);
|
||||
|
||||
const handleSigningOrderDisable = useCallback(() => {
|
||||
setShowSigningOrderConfirmation(false);
|
||||
|
||||
const currentSigners = form.getValues('signers');
|
||||
const updatedSigners = currentSigners.map((signer) => ({
|
||||
...signer,
|
||||
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
@ -384,11 +381,16 @@ export const AddSignersFormPartial = ({
|
||||
{...field}
|
||||
id="signingOrder"
|
||||
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked && hasAssistantRole) {
|
||||
setShowSigningOrderConfirmation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -613,7 +615,11 @@ export const AddSignersFormPartial = ({
|
||||
<FormControl>
|
||||
<RecipientRoleSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
isAssistantEnabled={isSigningOrderSequential}
|
||||
onValueChange={(value) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
handleRoleChange(index, value as RecipientRole)
|
||||
}
|
||||
disabled={
|
||||
snapshot.isDragging ||
|
||||
isSubmitting ||
|
||||
@ -710,6 +716,12 @@ export const AddSignersFormPartial = ({
|
||||
)}
|
||||
</Form>
|
||||
</AnimateGenericFadeInOut>
|
||||
|
||||
<SigningOrderConfirmation
|
||||
open={showSigningOrderConfirmation}
|
||||
onOpenChange={setShowSigningOrderConfirmation}
|
||||
onConfirm={handleSigningOrderDisable}
|
||||
/>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
|
||||
@ -59,10 +59,10 @@ export const FieldIcon = ({
|
||||
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
|
||||
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
|
||||
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
|
||||
} else if (fieldMeta.label) {
|
||||
label =
|
||||
fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
|
||||
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
|
||||
} else {
|
||||
label = fieldIcons[type]?.label;
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@documenso/ui/primitives/alert-dialog';
|
||||
|
||||
export type SigningOrderConfirmationProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export function SigningOrderConfirmation({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: SigningOrderConfirmationProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Warning</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have an assistant role on the signers list, removing the signing order will change
|
||||
the assistant role to signer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>Proceed</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@ -19,18 +19,18 @@ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, children: _children, ...props }, ref) => {
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input ring-offset-background focus:ring-ring h-4 w-4 rounded-full border focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="fill-primary text-primary h-2.5 w-2.5" />
|
||||
<Circle className="fill-primary h-2.5 w-2.5" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
|
||||
import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
|
||||
|
||||
import type { RecipientRole } from '.prisma/client';
|
||||
|
||||
@ -7,4 +7,5 @@ export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
||||
APPROVER: <BadgeCheck className="h-4 w-4" />,
|
||||
CC: <Copy className="h-4 w-4" />,
|
||||
VIEWER: <Eye className="h-4 w-4" />,
|
||||
ASSISTANT: <User className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
@ -32,7 +32,7 @@ import {
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -438,6 +438,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
APPROVER: [],
|
||||
ASSISTANT: [],
|
||||
};
|
||||
|
||||
recipients.forEach((recipient) => {
|
||||
@ -447,10 +448,25 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
return recipientsByRole;
|
||||
}, [recipients]);
|
||||
|
||||
useEffect(() => {
|
||||
const recipientsByRoleToDisplay = recipients.filter(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||
);
|
||||
|
||||
setSelectedSigner(
|
||||
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
|
||||
recipientsByRoleToDisplay[0],
|
||||
);
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRoleToDisplay = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
|
||||
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
|
||||
([role]) =>
|
||||
role !== RecipientRole.CC &&
|
||||
role !== RecipientRole.VIEWER &&
|
||||
role !== RecipientRole.ASSISTANT,
|
||||
);
|
||||
}, [recipientsByRole]);
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { toast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { Checkbox } from '../checkbox';
|
||||
import {
|
||||
@ -39,6 +40,7 @@ import {
|
||||
DocumentFlowFormContainerStep,
|
||||
} from '../document-flow/document-flow-root';
|
||||
import { ShowFieldItem } from '../document-flow/show-field-item';
|
||||
import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation';
|
||||
import type { DocumentFlowStep } from '../document-flow/types';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||
import { useStep } from '../stepper';
|
||||
@ -213,41 +215,30 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
const items = Array.from(watchedSigners);
|
||||
const [reorderedSigner] = items.splice(result.source.index, 1);
|
||||
|
||||
const insertIndex = result.destination.index;
|
||||
|
||||
items.splice(insertIndex, 0, reorderedSigner);
|
||||
|
||||
const updatedSigners = items.map((item, index) => ({
|
||||
...item,
|
||||
const updatedSigners = items.map((signer, index) => ({
|
||||
...signer,
|
||||
signingOrder: index + 1,
|
||||
}));
|
||||
|
||||
updatedSigners.forEach((item, index) => {
|
||||
const keys: (keyof typeof item)[] = [
|
||||
'formId',
|
||||
'nativeId',
|
||||
'email',
|
||||
'name',
|
||||
'role',
|
||||
'signingOrder',
|
||||
'actionAuth',
|
||||
];
|
||||
keys.forEach((key) => {
|
||||
form.setValue(`signers.${index}.${key}` as const, item[key]);
|
||||
});
|
||||
});
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
const currentLength = form.getValues('signers').length;
|
||||
if (currentLength > updatedSigners.length) {
|
||||
for (let i = updatedSigners.length; i < currentLength; i++) {
|
||||
form.unregister(`signers.${i}`);
|
||||
}
|
||||
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
await form.trigger('signers');
|
||||
},
|
||||
[form, watchedSigners],
|
||||
[form, watchedSigners, toast],
|
||||
);
|
||||
|
||||
const triggerDragAndDrop = useCallback(
|
||||
@ -308,26 +299,94 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
const handleSigningOrderChange = useCallback(
|
||||
(index: number, newOrderString: string) => {
|
||||
const newOrder = parseInt(newOrderString, 10);
|
||||
|
||||
if (!newOrderString.trim()) {
|
||||
const trimmedOrderString = newOrderString.trim();
|
||||
if (!trimmedOrderString) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newOrder)) {
|
||||
form.setValue(`signers.${index}.signingOrder`, index + 1);
|
||||
const newOrder = Number(trimmedOrderString);
|
||||
if (!Number.isInteger(newOrder) || newOrder < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = newOrder - 1;
|
||||
if (index !== newIndex) {
|
||||
updateSigningOrders(newIndex, index);
|
||||
triggerDragAndDrop(index, newIndex);
|
||||
const currentSigners = form.getValues('signers');
|
||||
const signer = currentSigners[index];
|
||||
|
||||
// Remove signer from current position and insert at new position
|
||||
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
|
||||
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
|
||||
remainingSigners.splice(newPosition, 0, signer);
|
||||
|
||||
const updatedSigners = remainingSigners.map((s, idx) => ({
|
||||
...s,
|
||||
signingOrder: idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[form, triggerDragAndDrop, updateSigningOrders],
|
||||
[form, toast],
|
||||
);
|
||||
|
||||
const handleRoleChange = useCallback(
|
||||
(index: number, role: RecipientRole) => {
|
||||
const currentSigners = form.getValues('signers');
|
||||
const signingOrder = form.getValues('signingOrder');
|
||||
|
||||
// Handle parallel to sequential conversion for assistants
|
||||
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||
toast({
|
||||
title: _(msg`Signing order is enabled.`),
|
||||
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSigners = currentSigners.map((signer, idx) => ({
|
||||
...signer,
|
||||
role: idx === index ? role : signer.role,
|
||||
signingOrder: idx + 1,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
|
||||
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||
toast({
|
||||
title: _(msg`Warning: Assistant as last signer`),
|
||||
description: _(
|
||||
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[form, toast],
|
||||
);
|
||||
|
||||
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
|
||||
|
||||
const handleSigningOrderDisable = useCallback(() => {
|
||||
setShowSigningOrderConfirmation(false);
|
||||
|
||||
const currentSigners = form.getValues('signers');
|
||||
const updatedSigners = currentSigners.map((signer) => ({
|
||||
...signer,
|
||||
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||
}));
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
@ -353,11 +412,19 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
{...field}
|
||||
id="signingOrder"
|
||||
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
||||
onCheckedChange={(checked) =>
|
||||
onCheckedChange={(checked) => {
|
||||
if (
|
||||
!checked &&
|
||||
watchedSigners.some((s) => s.role === RecipientRole.ASSISTANT)
|
||||
) {
|
||||
setShowSigningOrderConfirmation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -556,7 +623,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
<FormControl>
|
||||
<RecipientRoleSelect
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(value) =>
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
handleRoleChange(index, value as RecipientRole)
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
hideCCRecipients={isSignerDirectRecipient(signer)}
|
||||
/>
|
||||
@ -677,6 +747,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
onGoNextClick={() => void onFormSubmit()}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
|
||||
<SigningOrderConfirmation
|
||||
open={showSigningOrderConfirmation}
|
||||
onOpenChange={setShowSigningOrderConfirmation}
|
||||
onConfirm={handleSigningOrderDisable}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user