Merge branch 'main' into feat/document-table-filters

This commit is contained in:
David Nguyen
2025-07-23 14:41:41 +10:00
committed by GitHub
82 changed files with 10660 additions and 528 deletions

View File

@ -1,8 +1,3 @@
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---
## Description
<!--- Describe the changes introduced by this pull request. -->

6
.gitignore vendored
View File

@ -52,4 +52,8 @@ yarn-error.log*
!.vscode/extensions.json
# logs
logs.json
logs.json
# claude
.claude
CLAUDE.md

View File

@ -1,5 +1,6 @@
{
"index": "Get Started",
"authentication": "Authentication",
"rate-limits": "Rate Limits",
"versioning": "Versioning"
}

View File

@ -33,7 +33,7 @@ Our new API V2 supports the following typed SDKs:
<Callout type="info">
For the staging API, please use the following base URL:
`https://stg-app.documenso.dev/api/v2-beta/`
`https://stg-app.documenso.com/api/v2-beta/`
</Callout>
🚀 [V2 Announcement](https://documen.so/sdk-blog)

View File

@ -0,0 +1,54 @@
import { Callout } from 'nextra/components';
# Rate Limits
Documenso enforces rate limits on all API endpoints to ensure service stability.
## HTTP Rate Limits
**Limit:** 100 requests per minute per IP address
**Response:** 429 Too Many Requests
### Rate Limit Response
```json
{
"error": "Too many requests, please try again later."
}
```
<Callout type="warning">
No rate limit headers are currently provided. When you receive a 429 response, wait at least 60
seconds before retrying.
</Callout>
## Resource Limits
Beyond HTTP rate limits, your account has usage limits based on your subscription plan.
### Plan Limits
| Resource | Free | Paid | Self-hosted | Enterprise |
| ---------------- | ---- | --------- | ----------- | ---------- |
| Documents/month | 5 | Unlimited | Unlimited | Unlimited |
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
| Direct Templates | 3 | Unlimited | Unlimited | Unlimited |
### Error Response
When you exceed a resource limit:
```json
{
"error": "You have reached your document limit for this month. Please upgrade your plan.",
"code": "LIMIT_EXCEEDED",
"statusCode": 400
}
```
## Error Codes
| Code | Status | Description |
| ------------------- | ------ | ----------------------------- |
| `TOO_MANY_REQUESTS` | 429 | HTTP rate limit exceeded |
| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded |

View File

@ -619,6 +619,18 @@ Example payload for the `document.rejected` event:
}
```
## Webhook Events Testing
You can trigger test webhook events to test the webhook functionality. To trigger a test webhook, navigate to the [Webhooks page](/developers/webhooks) and click on the "Test Webhook" button.
![Documenso's Webhooks Page](/webhook-images/test-webhooks-page.webp)
This opens a dialog where you can select the event type to test.
![Documenso's individual webhook page](/webhook-images/test-webhook-dialog.webp)
Choose the appropriate event and click "Send Test Webhook." Youll shortly receive a test payload from Documenso with sample data.
## Availability
Webhooks are available to individual users and teams.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -127,6 +127,16 @@ export const DocumentMoveToFolderDialog = ({
return;
}
if (error.code === AppErrorCode.UNAUTHORIZED) {
toast({
title: _(msg`Error`),
description: _(msg`You are not allowed to move this document.`),
variant: 'destructive',
});
return;
}
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while moving the document.`),

View File

@ -116,8 +116,8 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
<Alert variant="destructive">
<AlertDescription>
<Trans>
This folder contains multiple items. Deleting it will also delete all items in the
folder, including nested folders and their contents.
This folder contains multiple items. Deleting it will remove all subfolders and move
all nested documents and templates to the root folder.
</Trans>
</AlertDescription>
</Alert>

View File

@ -1,8 +1,8 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@ -14,6 +14,7 @@ import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
@ -40,7 +41,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team';
export type FolderSettingsDialogProps = {
export type FolderUpdateDialogProps = {
folder: TFolderWithSubfolders | null;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
@ -53,12 +54,8 @@ export const ZUpdateFolderFormSchema = z.object({
export type TUpdateFolderFormSchema = z.infer<typeof ZUpdateFolderFormSchema>;
export const FolderSettingsDialog = ({
folder,
isOpen,
onOpenChange,
}: FolderSettingsDialogProps) => {
const { _ } = useLingui();
export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdateDialogProps) => {
const { t } = useLingui();
const team = useOptionalCurrentTeam();
const { toast } = useToast();
@ -84,7 +81,9 @@ export const FolderSettingsDialog = ({
}, [folder, form]);
const onFormSubmit = async (data: TUpdateFolderFormSchema) => {
if (!folder) return;
if (!folder) {
return;
}
try {
await updateFolder({
@ -96,7 +95,7 @@ export const FolderSettingsDialog = ({
});
toast({
title: _(msg`Folder updated successfully`),
title: t`Folder updated successfully`,
});
onOpenChange(false);
@ -105,7 +104,7 @@ export const FolderSettingsDialog = ({
if (error.code === AppErrorCode.NOT_FOUND) {
toast({
title: _(msg`Folder not found`),
title: t`Folder not found`,
});
}
}
@ -115,8 +114,12 @@ export const FolderSettingsDialog = ({
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Folder Settings</DialogTitle>
<DialogDescription>Manage the settings for this folder.</DialogDescription>
<DialogTitle>
<Trans>Folder Settings</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Manage the settings for this folder.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
@ -126,7 +129,9 @@ export const FolderSettingsDialog = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -141,19 +146,25 @@ export const FolderSettingsDialog = ({
name="visibility"
render={({ field }) => (
<FormItem>
<FormLabel>Visibility</FormLabel>
<FormLabel>
<Trans>Visibility</Trans>
</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select visibility" />
<SelectValue placeholder={t`Select visibility`} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={DocumentVisibility.EVERYONE}>Everyone</SelectItem>
<SelectItem value={DocumentVisibility.MANAGER_AND_ABOVE}>
Managers and above
<SelectItem value={DocumentVisibility.EVERYONE}>
<Trans>Everyone</Trans>
</SelectItem>
<SelectItem value={DocumentVisibility.MANAGER_AND_ABOVE}>
<Trans>Managers and above</Trans>
</SelectItem>
<SelectItem value={DocumentVisibility.ADMIN}>
<Trans>Admins only</Trans>
</SelectItem>
<SelectItem value={DocumentVisibility.ADMIN}>Admins only</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@ -163,7 +174,15 @@ export const FolderSettingsDialog = ({
)}
<DialogFooter>
<Button type="submit">Save Changes</Button>
<DialogClose asChild>
<Button variant="secondary">
<Trans>Cancel</Trans>
</Button>
</DialogClose>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update</Trans>
</Button>
</DialogFooter>
</form>
</Form>

View File

@ -19,6 +19,7 @@ import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
import { parseMessageDescriptorMacro } from '@documenso/lib/utils/i18n';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import { ZCreateOrganisationRequestSchema } from '@documenso/trpc/server/organisation-router/create-organisation.types';
import { cn } from '@documenso/ui/lib/utils';
@ -46,6 +47,8 @@ import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { IndividualPersonalLayoutCheckoutButton } from '../general/billing-plans';
export type OrganisationCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
@ -59,10 +62,11 @@ export type TCreateOrganisationFormSchema = z.infer<typeof ZCreateOrganisationFo
export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { refreshSession } = useSession();
const { refreshSession, organisations } = useSession();
const [searchParams] = useSearchParams();
const updateSearchParams = useUpdateSearchParams();
const isPersonalLayoutMode = isPersonalLayout(organisations);
const actionSearchParam = searchParams?.get('action');
@ -133,6 +137,13 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
form.reset();
}, [open, form]);
const isIndividualPlan = (priceId: string) => {
return (
plansData?.plans[INTERNAL_CLAIM_ID.INDIVIDUAL]?.monthlyPrice?.id === priceId ||
plansData?.plans[INTERNAL_CLAIM_ID.INDIVIDUAL]?.yearlyPrice?.id === priceId
);
};
return (
<Dialog
{...props}
@ -177,9 +188,15 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
<Trans>Cancel</Trans>
</Button>
<Button type="submit" onClick={() => setStep('create')}>
<Trans>Continue</Trans>
</Button>
{isIndividualPlan(selectedPriceId) && isPersonalLayoutMode ? (
<IndividualPersonalLayoutCheckoutButton priceId={selectedPriceId}>
<Trans>Checkout</Trans>
</IndividualPersonalLayoutCheckoutButton>
) : (
<Button type="submit" onClick={() => setStep('create')}>
<Trans>Continue</Trans>
</Button>
)}
</DialogFooter>
</fieldset>
</>
@ -306,7 +323,11 @@ const BillingPlanForm = ({
}, [value]);
const onBillingPeriodChange = (billingPeriod: 'monthlyPrice' | 'yearlyPrice') => {
const plan = dynamicPlans.find((plan) => plan[billingPeriod]?.id === value);
const plan = dynamicPlans.find(
(plan) =>
// Purposely using the opposite billing period to get the correct plan.
plan[billingPeriod === 'monthlyPrice' ? 'yearlyPrice' : 'monthlyPrice']?.id === value,
);
setBillingPeriod(billingPeriod);

View File

@ -13,7 +13,7 @@ import { z } from 'zod';
import { downloadFile } from '@documenso/lib/client-only/download-file';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { IS_BILLING_ENABLED, SUPPORT_EMAIL } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/organisations';
import { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
@ -303,8 +303,8 @@ export const OrganisationMemberInviteDialog = ({
<AlertDescription>
<Trans>
Your plan does not support inviting members. Please upgrade or your plan or
contact sales at <a href="mailto:support@documenso.com">support@documenso.com</a>{' '}
if you would like to discuss your options.
contact sales at <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> if you
would like to discuss your options.
</Trans>
</AlertDescription>
</Alert>

View File

@ -12,7 +12,11 @@ import type { z } from 'zod';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import {
IS_BILLING_ENABLED,
NEXT_PUBLIC_WEBAPP_URL,
SUPPORT_EMAIL,
} from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCreateTeamRequestSchema } from '@documenso/trpc/server/team-router/create-team.types';
@ -193,8 +197,8 @@ export const TeamCreateDialog = ({ trigger, onCreated, ...props }: TeamCreateDia
<AlertDescription className="mt-0">
<Trans>
You have reached the maximum number of teams for your plan. Please contact sales
at <a href="mailto:support@documenso.com">support@documenso.com</a> if you would
like to adjust your plan.
at <a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a> if you would like to
adjust your plan.
</Trans>
</AlertDescription>
</Alert>

View File

@ -0,0 +1,170 @@
import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Webhook } from '@prisma/client';
import { WebhookTriggerEvents } from '@prisma/client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export type WebhookTestDialogProps = {
webhook: Pick<Webhook, 'id' | 'webhookUrl' | 'eventTriggers'>;
children: React.ReactNode;
};
const ZTestWebhookFormSchema = z.object({
event: z.nativeEnum(WebhookTriggerEvents),
});
type TTestWebhookFormSchema = z.infer<typeof ZTestWebhookFormSchema>;
export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const team = useCurrentTeam();
const [open, setOpen] = useState(false);
const { mutateAsync: testWebhook } = trpc.webhook.testWebhook.useMutation();
const form = useForm<TTestWebhookFormSchema>({
resolver: zodResolver(ZTestWebhookFormSchema),
defaultValues: {
event: webhook.eventTriggers[0],
},
});
const onSubmit = async ({ event }: TTestWebhookFormSchema) => {
try {
await testWebhook({
id: webhook.id,
event,
teamId: team.id,
});
toast({
title: _(msg`Test webhook sent`),
description: _(msg`The test webhook has been successfully sent to your endpoint.`),
duration: 5000,
});
setOpen(false);
} catch (error) {
toast({
title: _(msg`Test webhook failed`),
description: _(
msg`We encountered an error while sending the test webhook. Please check your endpoint and try again.`,
),
variant: 'destructive',
duration: 5000,
});
}
};
return (
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Test Webhook</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Send a test webhook with sample data to verify your integration is working correctly.
</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField
control={form.control}
name="event"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Event Type</Trans>
</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an event type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{webhook.eventTriggers.map((event) => (
<SelectItem key={event} value={event}>
{toFriendlyWebhookEventName(event)}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="rounded-md border p-4">
<h4 className="mb-2 text-sm font-medium">
<Trans>Webhook URL</Trans>
</h4>
<p className="text-muted-foreground break-all text-sm">{webhook.webhookUrl}</p>
</div>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Send Test Webhook</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@ -10,7 +10,9 @@ import { useForm } from 'react-hook-form';
import type { InternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
@ -49,8 +51,12 @@ export type BillingPlansProps = {
export const BillingPlans = ({ plans }: BillingPlansProps) => {
const isMounted = useIsMounted();
const { organisations } = useSession();
const [interval, setInterval] = useState<'monthlyPrice' | 'yearlyPrice'>('yearlyPrice');
const isPersonalLayoutMode = isPersonalLayout(organisations);
const pricesToDisplay = useMemo(() => {
const prices = [];
@ -126,12 +132,18 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
<div className="flex-1" />
<BillingDialog
priceId={price.id}
planName={price.product.name}
memberCount={price.memberCount}
claim={price.claim}
/>
{isPersonalLayoutMode && price.claim === INTERNAL_CLAIM_ID.INDIVIDUAL ? (
<IndividualPersonalLayoutCheckoutButton priceId={price.id}>
<Trans>Subscribe</Trans>
</IndividualPersonalLayoutCheckoutButton>
) : (
<BillingDialog
priceId={price.id}
planName={price.product.name}
memberCount={price.memberCount}
claim={price.claim}
/>
)}
</CardContent>
</MotionCard>
))}
@ -315,3 +327,48 @@ const BillingDialog = ({
</Dialog>
);
};
/**
* Custom checkout button for individual organisations in personal layout mode.
*
* This is so they don't create an additional organisation which is not needed since
* it will clutter up the UI for them with unnecessary organisations.
*/
export const IndividualPersonalLayoutCheckoutButton = ({
priceId,
children,
}: {
priceId: string;
children: React.ReactNode;
}) => {
const { t } = useLingui();
const { toast } = useToast();
const { organisations } = useSession();
const { mutateAsync: createSubscription, isPending } =
trpc.billing.subscription.create.useMutation();
const onSubscribeClick = async () => {
try {
const createSubscriptionResponse = await createSubscription({
organisationId: organisations[0].id,
priceId,
isPersonalLayoutMode: true,
});
window.location.href = createSubscriptionResponse.redirectUrl;
} catch (_err) {
toast({
title: t`Something went wrong`,
description: t`An error occurred while trying to create a checkout session.`,
variant: 'destructive',
});
}
};
return (
<Button loading={isPending} onClick={() => void onSubscribeClick()}>
{children}
</Button>
);
};

View File

@ -17,6 +17,7 @@ import type {
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
import { Label } from '@documenso/ui/primitives/label';
@ -276,7 +277,14 @@ export const DocumentSigningCheckboxField = ({
{validationSign?.label} {checkboxValidationLength}
</FieldToolTip>
)}
<div className="z-50 my-0.5 flex flex-col gap-y-1">
<div
className={cn(
'z-50 my-0.5 flex gap-1',
parsedFieldMeta.direction === 'horizontal'
? 'flex-row flex-wrap'
: 'flex-col gap-y-1',
)}
>
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`;
@ -305,7 +313,12 @@ export const DocumentSigningCheckboxField = ({
)}
{field.inserted && (
<div className="my-0.5 flex flex-col gap-y-1">
<div
className={cn(
'my-0.5 flex gap-1',
parsedFieldMeta.direction === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col gap-y-1',
)}
>
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`;

View File

@ -54,7 +54,7 @@ export const DocumentSigningNumberField = ({
const { toast } = useToast();
const { revalidate } = useRevalidator();
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
const [showNumberModal, setShowNumberModal] = useState(false);
@ -62,8 +62,8 @@ export const DocumentSigningNumberField = ({
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const defaultValue = parsedFieldMeta?.value;
const [localNumber, setLocalNumber] = useState(
parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0',
const [localNumber, setLocalNumber] = useState(() =>
parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '',
);
const initialErrors: ValidationErrors = {
@ -213,16 +213,13 @@ export const DocumentSigningNumberField = ({
useEffect(() => {
if (!showNumberModal) {
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '');
setErrors(initialErrors);
}
}, [showNumberModal]);
useEffect(() => {
if (
(!field.inserted && defaultValue && localNumber) ||
(!field.inserted && parsedFieldMeta?.readOnly && defaultValue)
) {
if (!field.inserted && defaultValue) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
actionTarget: field.type,
@ -318,7 +315,7 @@ export const DocumentSigningNumberField = ({
variant="secondary"
onClick={() => {
setShowNumberModal(false);
setLocalNumber('');
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '');
}}
>
<Trans>Cancel</Trans>

View File

@ -72,9 +72,11 @@ export const DocumentSigningPageView = ({
}
const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId);
const targetSigner =
recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
return (
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={targetSigner}>
<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"

View File

@ -38,11 +38,6 @@ export const DocumentSigningRecipientProvider = ({
recipient,
targetSigner = null,
}: DocumentSigningRecipientProviderProps) => {
// console.log({
// recipient,
// targetSigner,
// isAssistantMode: !!targetSigner,
// });
return (
<DocumentSigningRecipientContext.Provider
value={{

View File

@ -13,7 +13,7 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
import { DocumentUploadDropzone } from '~/components/general/document/document-upload';
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
@ -219,7 +219,7 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
}}
/>
<FolderSettingsDialog
<FolderUpdateDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {

View File

@ -5,18 +5,23 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { SubscriptionStatus } from '@prisma/client';
import { AlertTriangle } from 'lucide-react';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
import { useOptionalCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
@ -86,7 +91,7 @@ export const OrganisationBillingBanner = () => {
className={cn({
'text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-500':
subscriptionStatus === SubscriptionStatus.PAST_DUE,
'text-destructive-foreground hover:bg-destructive-foreground hover:text-white':
'text-destructive-foreground hover:bg-destructive hover:text-white':
subscriptionStatus === SubscriptionStatus.INACTIVE,
})}
disabled={isPending}
@ -99,38 +104,78 @@ export const OrganisationBillingBanner = () => {
</div>
<Dialog open={isOpen} onOpenChange={(value) => !isPending && setIsOpen(value)}>
<DialogContent>
<DialogTitle>
<Trans>Payment overdue</Trans>
</DialogTitle>
{match(subscriptionStatus)
.with(SubscriptionStatus.PAST_DUE, () => (
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Payment overdue</Trans>
</DialogTitle>
{match(subscriptionStatus)
.with(SubscriptionStatus.PAST_DUE, () => (
<DialogDescription>
<Trans>
Your payment for teams is overdue. Please settle the payment to avoid any service
disruptions.
</Trans>
</DialogDescription>
))
.with(SubscriptionStatus.INACTIVE, () => (
<DialogDescription>
<Trans>
Due to an unpaid invoice, your team has been restricted. Please settle the payment
to restore full access to your team.
</Trans>
</DialogDescription>
))
.otherwise(() => null)}
<DialogDescription>
<Trans>
Your payment is overdue. Please settle the payment to avoid any service
disruptions.
</Trans>
</DialogDescription>
</DialogHeader>
{canExecuteOrganisationAction('MANAGE_BILLING', organisation.currentOrganisationRole) && (
<DialogFooter>
<Button loading={isPending} onClick={async () => handleCreatePortal(organisation.id)}>
<Trans>Resolve payment</Trans>
</Button>
</DialogFooter>
)}
</DialogContent>
{canExecuteOrganisationAction(
'MANAGE_BILLING',
organisation.currentOrganisationRole,
) && (
<DialogFooter>
<Button
loading={isPending}
onClick={async () => handleCreatePortal(organisation.id)}
>
<Trans>Resolve payment</Trans>
</Button>
</DialogFooter>
)}
</DialogContent>
))
.with(SubscriptionStatus.INACTIVE, () => (
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Subscription invalid</Trans>
</DialogTitle>
<DialogDescription>
<Trans>
Your plan is no longer valid. Please subscribe to a new plan to continue using
Documenso.
</Trans>
</DialogDescription>
</DialogHeader>
<Alert variant="neutral">
<AlertDescription>
<Trans>
If there is any issue with your subscription, please contact us at{' '}
<a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>.
</Trans>
</AlertDescription>
</Alert>
{canExecuteOrganisationAction(
'MANAGE_BILLING',
organisation.currentOrganisationRole,
) && (
<DialogFooter>
<DialogClose asChild>
<Button asChild>
<Link to={`/o/${organisation.url}/settings/billing`}>
<Trans>Manage Billing</Trans>
</Link>
</Button>
</DialogClose>
</DialogFooter>
)}
</DialogContent>
))
.otherwise(() => null)}
</Dialog>
</>
);

View File

@ -188,7 +188,7 @@ export const DocumentsTableActionDropdown = ({
<Trans>Duplicate</Trans>
</DropdownMenuItem>
{onMoveDocument && (
{onMoveDocument && canManageDocument && (
<DropdownMenuItem onClick={onMoveDocument} onSelect={(e) => e.preventDefault()}>
<FolderInput className="mr-2 h-4 w-4" />
<Trans>Move to Folder</Trans>

View File

@ -1,5 +1,6 @@
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { SubscriptionStatus } from '@prisma/client';
import { Loader } from 'lucide-react';
import type Stripe from 'stripe';
import { match } from 'ts-pattern';
@ -134,7 +135,9 @@ export default function TeamsSettingBillingPage() {
<hr className="my-4" />
{!subscription && canManageBilling && <BillingPlans plans={plans} />}
{(!subscription ||
subscription.organisationSubscription.status === SubscriptionStatus.INACTIVE) &&
canManageBilling && <BillingPlans plans={plans} />}
<section className="mt-6">
<OrganisationBillingInvoicesTable

View File

@ -14,7 +14,7 @@ import { Input } from '@documenso/ui/primitives/input';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
import { FolderCard } from '~/components/general/folder/folder-card';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
@ -177,7 +177,7 @@ export default function DocumentsFoldersPage() {
}}
/>
<FolderSettingsDialog
<FolderUpdateDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open) => {

View File

@ -2,13 +2,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { useRevalidator } from 'react-router';
import { Link } from 'react-router';
import type { z } from 'zod';
import { trpc } from '@documenso/trpc/react';
import { ZEditWebhookRequestSchema } from '@documenso/trpc/server/webhook-router/schema';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Form,
@ -21,9 +22,12 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
import { Switch } from '@documenso/ui/primitives/switch';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { WebhookTestDialog } from '~/components/dialogs/webhook-test-dialog';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
import { SettingsHeader } from '~/components/general/settings-header';
import { WebhookMultiSelectCombobox } from '~/components/general/webhook-multiselect-combobox';
import { useCurrentTeam } from '~/providers/team';
@ -92,25 +96,45 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
}
};
if (isLoading) {
return <SpinnerBox className="py-32" />;
}
// Todo: Update UI, currently out of place.
if (!webhook) {
return (
<GenericErrorLayout
errorCode={404}
errorCodeMap={{
404: {
heading: msg`Webhook not found`,
subHeading: msg`404 Webhook not found`,
message: msg`The webhook you are looking for may have been removed, renamed or may have never
existed.`,
},
}}
primaryButton={
<Button asChild>
<Link to={`/t/${team.url}/settings/webhooks`}>
<Trans>Go back</Trans>
</Link>
</Button>
}
secondaryButton={null}
/>
);
}
return (
<div>
<div className="max-w-2xl">
<SettingsHeader
title={_(msg`Edit webhook`)}
subtitle={_(msg`On this page, you can edit the webhook and its settings.`)}
/>
{isLoading && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
</div>
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset
className="flex h-full max-w-xl flex-col gap-y-6"
disabled={form.formState.isSubmitting}
>
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
<div className="flex flex-col-reverse gap-4 md:flex-row">
<FormField
control={form.control}
@ -203,7 +227,7 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
)}
/>
<div className="mt-4">
<div className="flex justify-end gap-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update webhook</Trans>
</Button>
@ -211,6 +235,30 @@ export default function WebhookPage({ params }: Route.ComponentProps) {
</fieldset>
</form>
</Form>
<Alert
className="mt-6 flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
variant="neutral"
>
<div>
<AlertTitle>
<Trans>Test Webhook</Trans>
</AlertTitle>
<AlertDescription className="mr-2">
<Trans>
Send a test webhook with sample data to verify your integration is working correctly.
</Trans>
</AlertDescription>
</div>
<div className="flex-shrink-0">
<WebhookTestDialog webhook={webhook}>
<Button variant="outline" disabled={!webhook.enabled}>
<Trans>Test Webhook</Trans>
</Button>
</WebhookTestDialog>
</div>
</Alert>
</div>
);
}

View File

@ -54,7 +54,7 @@ export default function WebhookPage() {
</div>
)}
{webhooks && webhooks.length > 0 && (
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
<div className="mt-4 flex max-w-2xl flex-col gap-y-4">
{webhooks?.map((webhook) => (
<div
key={webhook.id}

View File

@ -14,7 +14,7 @@ import { Input } from '@documenso/ui/primitives/input';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
import { FolderSettingsDialog } from '~/components/dialogs/folder-settings-dialog';
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
import { FolderCard } from '~/components/general/folder/folder-card';
import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
@ -177,7 +177,7 @@ export default function TemplatesFoldersPage() {
}}
/>
<FolderSettingsDialog
<FolderUpdateDialog
folder={folderToSettings}
isOpen={isSettingsFolderOpen}
onOpenChange={(open: boolean) => {

View File

@ -1,10 +1,9 @@
import { Trans } from '@lingui/react/macro';
import { Link } from 'react-router';
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
import { Button } from '@documenso/ui/primitives/button';
const SUPPORT_EMAIL = 'support@documenso.com';
export default function SignatureDisclosure() {
return (
<div>

View File

@ -101,5 +101,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "1.12.2-rc.0"
"version": "1.12.2-rc.2"
}

6
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "1.12.2-rc.0",
"version": "1.12.2-rc.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "1.12.2-rc.0",
"version": "1.12.2-rc.2",
"workspaces": [
"apps/*",
"packages/*"
@ -89,7 +89,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "1.12.2-rc.0",
"version": "1.12.2-rc.2",
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.12.2-rc.0",
"version": "1.12.2-rc.2",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -1,5 +1,10 @@
import { initContract } from '@ts-rest/core';
import {
ZCreateTemplateV2RequestSchema,
ZCreateTemplateV2ResponseSchema,
} from '@documenso/trpc/server/template-router/schema';
import {
ZAuthorizationHeadersSchema,
ZCreateDocumentFromTemplateMutationResponseSchema,
@ -87,6 +92,18 @@ export const ApiContractV1 = c.router(
summary: 'Upload a new document and get a presigned URL',
},
createTemplate: {
method: 'POST',
path: '/api/v1/templates',
body: ZCreateTemplateV2RequestSchema,
responses: {
200: ZCreateTemplateV2ResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
},
summary: 'Create a new template and get a presigned URL',
},
deleteTemplate: {
method: 'DELETE',
path: '/api/v1/templates/:id',

View File

@ -30,6 +30,7 @@ import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-
import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy';
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
@ -400,6 +401,109 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}
}),
createTemplate: authenticatedMiddleware(async (args, user, team) => {
const { body } = args;
const {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
meta,
} = body;
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
return {
status: 500,
body: {
message: 'Create template is not available without S3 transport.',
},
};
}
const dateFormat = meta?.dateFormat
? DATE_FORMATS.find((format) => format.value === meta?.dateFormat)
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
if (meta?.dateFormat && !dateFormat) {
return {
status: 400,
body: {
message: 'Invalid date format. Please provide a valid date format',
},
};
}
const timezone = meta?.timezone
? TIME_ZONES.find((tz) => tz === meta?.timezone)
: DEFAULT_DOCUMENT_TIME_ZONE;
const isTimeZoneValid = meta?.timezone ? TIME_ZONES.includes(String(timezone)) : true;
if (!isTimeZoneValid) {
return {
status: 400,
body: {
message: 'Invalid timezone. Please provide a valid timezone',
},
};
}
const fileName = title?.endsWith('.pdf') ? title : `${title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const templateDocumentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const createdTemplate = await createTemplate({
userId: user.id,
teamId: team.id,
templateDocumentDataId: templateDocumentData.id,
data: {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
},
meta,
});
const fullTemplate = await getTemplateById({
id: createdTemplate.id,
userId: user.id,
teamId: team.id,
});
return {
status: 200,
body: {
uploadUrl: url,
template: fullTemplate,
},
};
} catch (err) {
return {
status: 404,
body: {
message: 'An error has occured while creating the template',
},
};
}
}),
deleteTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
const { id: templateId } = args.params;
@ -1432,7 +1536,6 @@ const updateDocument = async ({
return await prisma.document.update({
where: {
id: documentId,
userId,
team: buildTeamWhereQuery({ teamId, userId }),
},
data: {

View File

@ -168,7 +168,7 @@ test('[TEAMS]: can rename a document folder', async ({ page }) => {
await page.getByRole('menuitem', { name: 'Settings' }).click();
await page.getByLabel('Name').fill('Team Archive');
await page.getByRole('button', { name: 'Save Changes' }).click();
await page.getByRole('button', { name: 'Update' }).click();
await expect(page.getByText('Team Archive')).toBeVisible();
});
@ -470,7 +470,7 @@ test('[TEAMS]: can rename a template folder', async ({ page }) => {
await page.getByRole('menuitem', { name: 'Settings' }).click();
await page.getByLabel('Name').fill('Updated Team Template Folder');
await page.getByRole('button', { name: 'Save Changes' }).click();
await page.getByRole('button', { name: 'Update' }).click();
await expect(page.getByText('Updated Team Template Folder')).toBeVisible();
});

View File

@ -1,13 +0,0 @@
import { z } from 'zod';
export const ZEarlyAdopterCheckoutMetadataSchema = z.object({
name: z.string(),
email: z.string(),
signatureText: z.string(),
signatureDataUrl: z.string().optional(),
source: z.literal('marketing'),
});
export type TEarlyAdopterCheckoutMetadataSchema = z.infer<
typeof ZEarlyAdopterCheckoutMetadataSchema
>;

View File

@ -81,17 +81,34 @@ export const onSubscriptionCreated = async ({ subscription }: OnSubscriptionCrea
const status = match(subscription.status)
.with('active', () => SubscriptionStatus.ACTIVE)
.with('trialing', () => SubscriptionStatus.ACTIVE)
.with('past_due', () => SubscriptionStatus.PAST_DUE)
.otherwise(() => SubscriptionStatus.INACTIVE);
await prisma.subscription.create({
data: {
const periodEnd =
subscription.status === 'trialing' && subscription.trial_end
? new Date(subscription.trial_end * 1000)
: new Date(subscription.current_period_end * 1000);
await prisma.subscription.upsert({
where: {
organisationId,
},
create: {
organisationId,
status,
customerId,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
periodEnd,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
update: {
status,
customerId,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
});
@ -172,14 +189,17 @@ const handleOrganisationUpdate = async ({ customerId, claim }: HandleOrganisatio
}
// Todo: logging
if (organisation.subscription) {
console.error('Organisation already has a subscription');
if (
organisation.subscription &&
organisation.subscription.status !== SubscriptionStatus.INACTIVE
) {
console.error('Organisation already has an active subscription');
// This should never happen
throw Response.json(
{
success: false,
message: `Organisation already has a subscription`,
message: `Organisation already has an active subscription`,
} satisfies StripeWebhookResponse,
{ status: 500 },
);

View File

@ -1,8 +1,9 @@
import { SubscriptionStatus } from '@prisma/client';
import { OrganisationType, SubscriptionStatus } from '@prisma/client';
import { match } from 'ts-pattern';
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
import { type Stripe, stripe } from '@documenso/lib/server-only/stripe';
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
import { prisma } from '@documenso/prisma';
export type OnSubscriptionUpdatedOptions = {
@ -55,8 +56,12 @@ export const onSubscriptionUpdated = async ({
);
}
if (organisation.subscription?.planId !== subscription.id) {
console.error('[WARNING]: Organisation has two subscriptions');
if (
organisation.subscription &&
organisation.subscription.status !== SubscriptionStatus.INACTIVE &&
organisation.subscription.planId !== subscription.id
) {
console.error('[WARNING]: Organisation might have two subscriptions');
}
const previousItem = previousAttributes?.items?.data[0];
@ -83,20 +88,41 @@ export const onSubscriptionUpdated = async ({
const status = match(subscription.status)
.with('active', () => SubscriptionStatus.ACTIVE)
.with('trialing', () => SubscriptionStatus.ACTIVE)
.with('past_due', () => SubscriptionStatus.PAST_DUE)
.otherwise(() => SubscriptionStatus.INACTIVE);
const periodEnd =
subscription.status === 'trialing' && subscription.trial_end
? new Date(subscription.trial_end * 1000)
: new Date(subscription.current_period_end * 1000);
// Migrate the organisation type if it is no longer an individual plan.
if (
updatedSubscriptionClaim.id !== INTERNAL_CLAIM_ID.INDIVIDUAL &&
updatedSubscriptionClaim.id !== INTERNAL_CLAIM_ID.FREE &&
organisation.type === OrganisationType.PERSONAL
) {
await prisma.organisation.update({
where: {
id: organisation.id,
},
data: {
type: OrganisationType.ORGANISATION,
},
});
}
await prisma.$transaction(async (tx) => {
await tx.subscription.update({
where: {
planId: subscription.id,
organisationId: organisation.id,
},
data: {
organisationId: organisation.id,
status: status,
planId: subscription.id,
priceId: subscription.items.data[0].price.id,
periodEnd: new Date(subscription.current_period_end * 1000),
periodEnd,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
});

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
@ -10,7 +10,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
width: 0,
});
const calculateBounds = () => {
const calculateBounds = useCallback(() => {
const $el =
typeof elementOrSelector === 'string'
? document.querySelector<HTMLElement>(elementOrSelector)
@ -32,11 +32,11 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
width,
height,
};
};
}, [elementOrSelector, withScroll]);
useEffect(() => {
setBounds(calculateBounds());
}, [calculateBounds]);
}, []);
useEffect(() => {
const onResize = () => {
@ -48,7 +48,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
return () => {
window.removeEventListener('resize', onResize);
};
}, [calculateBounds]);
}, []);
useEffect(() => {
const $el =
@ -69,7 +69,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
return () => {
observer.disconnect();
};
}, [calculateBounds]);
}, []);
return bounds;
};

View File

@ -12,3 +12,5 @@ export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
export const API_V2_BETA_URL = '/api/v2-beta';
export const SUPPORT_EMAIL = 'support@documenso.com';

View File

@ -16,6 +16,7 @@ import { prefixedId } from '../../universal/id';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamById } from '../team/get-team';
import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@ -58,8 +59,10 @@ export const createDocument = async ({
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
},
select: {
visibility: true,

View File

@ -54,14 +54,7 @@ export const resendDocument = async ({
const document = await prisma.document.findUnique({
where: documentWhereInput,
include: {
recipients: {
where: {
id: {
in: recipients,
},
signingStatus: SigningStatus.NOT_SIGNED,
},
},
recipients: true,
documentMeta: true,
team: {
select: {
@ -90,6 +83,11 @@ export const resendDocument = async ({
throw new Error('Can not send completed document');
}
const recipientsToRemind = document.recipients.filter(
(recipient) =>
recipients.includes(recipient.id) && recipient.signingStatus === SigningStatus.NOT_SIGNED,
);
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientSigningRequest;
@ -106,7 +104,7 @@ export const resendDocument = async ({
});
await Promise.all(
document.recipients.map(async (recipient) => {
recipientsToRemind.map(async (recipient) => {
if (recipient.role === RecipientRole.CC) {
return;
}

View File

@ -26,7 +26,6 @@ export const deleteField = async ({
id: fieldId,
document: {
id: documentId,
userId,
team: buildTeamWhereQuery({ teamId, userId }),
},
},

View File

@ -48,7 +48,6 @@ export const updateField = async ({
id: fieldId,
document: {
id: documentId,
userId,
team: buildTeamWhereQuery({ teamId, userId }),
},
},

View File

@ -4,6 +4,7 @@ import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamById } from '../team/get-team';
export interface DeleteFolderOptions {
@ -18,8 +19,10 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
},
include: {
documents: true,

View File

@ -2,6 +2,8 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface MoveFolderOptions {
userId: number;
teamId?: number;
@ -15,8 +17,10 @@ export const moveFolder = async ({ userId, teamId, folderId, parentId }: MoveFol
const folder = await tx.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
},
});

View File

@ -2,6 +2,8 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { FolderType } from '@documenso/lib/types/folder-type';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface MoveTemplateToFolderOptions {
userId: number;
teamId?: number;
@ -15,45 +17,47 @@ export const moveTemplateToFolder = async ({
templateId,
folderId,
}: MoveTemplateToFolderOptions) => {
return await prisma.$transaction(async (tx) => {
const template = await tx.template.findFirst({
where: {
id: templateId,
userId,
const template = await prisma.template.findFirst({
where: {
id: templateId,
team: buildTeamWhereQuery({
teamId,
},
userId,
}),
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
if (folderId !== null) {
const folder = await tx.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
type: FolderType.TEMPLATE,
},
});
if (!folder) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found',
});
}
}
return await tx.template.update({
if (folderId !== null) {
const folder = await prisma.folder.findFirst({
where: {
id: templateId,
},
data: {
folderId,
id: folderId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
type: FolderType.TEMPLATE,
},
});
if (!folder) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Folder not found',
});
}
}
return await prisma.template.update({
where: {
id: templateId,
},
data: {
folderId,
},
});
};

View File

@ -2,6 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import type { TFolderType } from '../../types/folder-type';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface PinFolderOptions {
userId: number;
@ -14,8 +15,10 @@ export const pinFolder = async ({ userId, teamId, folderId, type }: PinFolderOpt
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
type,
},
});

View File

@ -2,6 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import type { TFolderType } from '../../types/folder-type';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface UnpinFolderOptions {
userId: number;
@ -14,8 +15,10 @@ export const unpinFolder = async ({ userId, teamId, folderId, type }: UnpinFolde
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
type,
},
});

View File

@ -4,6 +4,7 @@ import { DocumentVisibility } from '@documenso/prisma/generated/types';
import type { TFolderType } from '../../types/folder-type';
import { FolderType } from '../../types/folder-type';
import { buildTeamWhereQuery } from '../../utils/teams';
export interface UpdateFolderOptions {
userId: number;
@ -25,8 +26,10 @@ export const updateFolder = async ({
const folder = await prisma.folder.findFirst({
where: {
id: folderId,
userId,
teamId,
team: buildTeamWhereQuery({
teamId,
userId,
}),
type,
},
});

View File

@ -1,47 +0,0 @@
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { prisma } from '@documenso/prisma';
export type CreateTeamBillingPortalOptions = {
userId: number;
teamId: number;
};
export const createTeamBillingPortal = async ({
userId,
teamId,
}: CreateTeamBillingPortalOptions) => {
if (!IS_BILLING_ENABLED()) {
throw new Error('Billing is not enabled');
}
const team = await prisma.team.findFirstOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_BILLING'],
},
},
},
},
include: {
subscription: true,
},
});
if (!team.subscription) {
throw new Error('Team has no subscription');
}
if (!team.customerId) {
throw new Error('Team has no customerId');
}
return getPortalSession({
customerId: team.customerId,
});
};

View File

@ -240,35 +240,79 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
}));
const selected: string[] = fromCheckboxValue(field.customText);
const direction = meta.data.direction ?? 'vertical';
const topPadding = 12;
const leftCheckboxPadding = 8;
const leftCheckboxLabelPadding = 12;
const checkboxSpaceY = 13;
for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * checkboxSpaceY + topPadding;
if (direction === 'horizontal') {
// Horizontal layout: arrange checkboxes side by side with wrapping
let currentX = leftCheckboxPadding;
let currentY = topPadding;
const maxWidth = pageWidth - fieldX - leftCheckboxPadding * 2;
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
for (const [index, item] of (values ?? []).entries()) {
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
if (selected.includes(item.value)) {
checkbox.check();
if (selected.includes(item.value)) {
checkbox.check();
}
const labelText = item.value.includes('empty-value-') ? '' : item.value;
const labelWidth = font.widthOfTextAtSize(labelText, 12);
const itemWidth = leftCheckboxLabelPadding + labelWidth + 16; // checkbox + padding + label + margin
// Check if item fits on current line, if not wrap to next line
if (currentX + itemWidth > maxWidth && index > 0) {
currentX = leftCheckboxPadding;
currentY += checkboxSpaceY;
}
page.drawText(labelText, {
x: fieldX + currentX + leftCheckboxLabelPadding,
y: pageHeight - (fieldY + currentY),
size: 12,
font,
rotate: degrees(pageRotationInDegrees),
});
checkbox.addToPage(page, {
x: fieldX + currentX,
y: pageHeight - (fieldY + currentY),
height: 8,
width: 8,
});
currentX += itemWidth;
}
} else {
// Vertical layout: original behavior
for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * checkboxSpaceY + topPadding;
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
x: fieldX + leftCheckboxPadding + leftCheckboxLabelPadding,
y: pageHeight - (fieldY + offsetY),
size: 12,
font,
rotate: degrees(pageRotationInDegrees),
});
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
checkbox.addToPage(page, {
x: fieldX + leftCheckboxPadding,
y: pageHeight - (fieldY + offsetY),
height: 8,
width: 8,
});
if (selected.includes(item.value)) {
checkbox.check();
}
page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
x: fieldX + leftCheckboxPadding + leftCheckboxLabelPadding,
y: pageHeight - (fieldY + offsetY),
size: 12,
font,
rotate: degrees(pageRotationInDegrees),
});
checkbox.addToPage(page, {
x: fieldX + leftCheckboxPadding,
y: pageHeight - (fieldY + offsetY),
height: 8,
width: 8,
});
}
}
})
.with({ type: FieldType.RADIO }, (field) => {

View File

@ -228,6 +228,7 @@ const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillField
type: 'checkbox',
label: field.label,
values: newValues,
direction: checkboxMeta.direction ?? 'vertical',
};
return meta;

View File

@ -1,16 +1,31 @@
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//TemplateSchema';
import type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import { createDocumentAuthOptions } from '../../utils/document-auth';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamSettings } from '../team/get-team-settings';
export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
export type CreateTemplateOptions = {
userId: number;
teamId: number;
templateDocumentDataId: string;
data: {
title: string;
folderId?: string;
externalId?: string | null;
visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[];
publicTitle?: string;
publicDescription?: string;
type?: Template['type'];
};
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>;
};
export const ZCreateTemplateResponseSchema = TemplateSchema;
@ -18,12 +33,14 @@ export const ZCreateTemplateResponseSchema = TemplateSchema;
export type TCreateTemplateResponse = z.infer<typeof ZCreateTemplateResponseSchema>;
export const createTemplate = async ({
title,
userId,
teamId,
templateDocumentDataId,
folderId,
data,
meta = {},
}: CreateTemplateOptions) => {
const { title, folderId } = data;
const team = await prisma.team.findFirst({
where: buildTeamWhereQuery({ teamId, userId }),
});
@ -55,16 +72,27 @@ export const createTemplate = async ({
return await prisma.template.create({
data: {
title,
teamId,
userId,
templateDocumentDataId,
teamId,
folderId: folderId,
folderId,
externalId: data.externalId,
visibility: data.visibility ?? settings.documentVisibility,
authOptions: createDocumentAuthOptions({
globalAccessAuth: data.globalAccessAuth || [],
globalActionAuth: data.globalActionAuth || [],
}),
publicTitle: data.publicTitle,
publicDescription: data.publicDescription,
type: data.type,
templateMeta: {
create: {
language: settings.documentLanguage,
typedSignatureEnabled: settings.typedSignatureEnabled,
uploadSignatureEnabled: settings.uploadSignatureEnabled,
drawSignatureEnabled: settings.drawSignatureEnabled,
...meta,
language: meta?.language ?? settings.documentLanguage,
typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
emailSettings: meta?.emailSettings || undefined,
},
},
},

View File

@ -2,6 +2,8 @@ import type { WebhookTriggerEvents } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { buildTeamWhereQuery } from '../../utils/teams';
export type GetAllWebhooksByEventTriggerOptions = {
event: WebhookTriggerEvents;
userId: number;
@ -19,22 +21,10 @@ export const getAllWebhooksByEventTrigger = async ({
eventTriggers: {
has: event,
},
team: {
id: teamId,
teamGroups: {
some: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
},
team: buildTeamWhereQuery({
teamId,
userId,
}),
},
});
};

View File

@ -1,5 +1,8 @@
import { prisma } from '@documenso/prisma';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
import { buildTeamWhereQuery } from '../../utils/teams';
export type GetWebhookByIdOptions = {
id: string;
userId: number;
@ -10,23 +13,11 @@ export const getWebhookById = async ({ id, userId, teamId }: GetWebhookByIdOptio
return await prisma.webhook.findFirstOrThrow({
where: {
id,
userId,
team: {
id: teamId,
teamGroups: {
some: {
organisationGroup: {
organisationGroupMembers: {
some: {
organisationMember: {
userId,
},
},
},
},
},
},
},
team: buildTeamWhereQuery({
teamId,
userId,
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
}),
},
});
};

View File

@ -0,0 +1,44 @@
import type { WebhookTriggerEvents } from '@prisma/client';
import { getWebhookById } from './get-webhook-by-id';
import { generateSampleWebhookPayload } from './trigger/generate-sample-data';
import { triggerWebhook } from './trigger/trigger-webhook';
export type TriggerTestWebhookOptions = {
id: string;
event: WebhookTriggerEvents;
userId: number;
teamId: number;
};
export const triggerTestWebhook = async ({
id,
event,
userId,
teamId,
}: TriggerTestWebhookOptions) => {
const webhook = await getWebhookById({ id, userId, teamId });
if (!webhook.enabled) {
throw new Error('Webhook is disabled');
}
if (!webhook.eventTriggers.includes(event)) {
throw new Error(`Webhook does not support event: ${event}`);
}
const samplePayload = generateSampleWebhookPayload(event, webhook.webhookUrl);
try {
await triggerWebhook({
event,
data: samplePayload,
userId,
teamId,
});
return { success: true, message: 'Test webhook triggered successfully' };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
};

View File

@ -0,0 +1,485 @@
import {
DocumentDistributionMethod,
DocumentSigningOrder,
DocumentSource,
DocumentStatus,
DocumentVisibility,
ReadStatus,
RecipientRole,
SendStatus,
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import type { WebhookPayload } from '../../../types/webhook-payload';
export const generateSampleWebhookPayload = (
event: WebhookTriggerEvents,
webhookUrl: string,
): WebhookPayload => {
const now = new Date();
const basePayload = {
id: 10,
externalId: null,
userId: 1,
authOptions: null,
formValues: null,
visibility: DocumentVisibility.EVERYONE,
title: 'documenso.pdf',
status: DocumentStatus.DRAFT,
documentDataId: 'hs8qz1ktr9204jn7mg6c5dxy0',
createdAt: now,
updatedAt: now,
completedAt: null,
deletedAt: null,
teamId: null,
templateId: null,
source: DocumentSource.DOCUMENT,
documentMeta: {
id: 'doc_meta_123',
subject: 'Please sign this document',
message: 'Hello, please review and sign this document.',
timezone: 'UTC',
password: null,
dateFormat: 'MM/DD/YYYY',
redirectUrl: null,
signingOrder: DocumentSigningOrder.PARALLEL,
allowDictateNextSigner: false,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,
drawSignatureEnabled: true,
language: 'en',
distributionMethod: DocumentDistributionMethod.EMAIL,
emailSettings: null,
},
recipients: [
{
id: 52,
documentId: 10,
templateId: null,
email: 'signer@documenso.com',
name: 'John Doe',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: null,
signingOrder: 1,
rejectionReason: null,
role: RecipientRole.SIGNER,
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
sendStatus: SendStatus.NOT_SENT,
},
],
Recipient: [
{
id: 52,
documentId: 10,
templateId: null,
email: 'signer@documenso.com',
name: 'John Doe',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: null,
signingOrder: 1,
rejectionReason: null,
role: RecipientRole.SIGNER,
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
sendStatus: SendStatus.NOT_SENT,
},
],
};
if (event === WebhookTriggerEvents.DOCUMENT_CREATED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.DRAFT,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_SENT) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
email: 'signer2@documenso.com',
name: 'Signer 2',
role: RecipientRole.VIEWER,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: null,
signingOrder: 1,
rejectionReason: null,
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
Recipient: [
{
...basePayload.Recipient[0],
email: 'signer1@documenso.com',
name: 'Signer 1',
token: 'SIGNING_TOKEN',
signingOrder: 2,
role: RecipientRole.SIGNER,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: null,
rejectionReason: null,
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_OPENED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
email: 'signer2@documenso.com',
name: 'Signer 2',
role: RecipientRole.VIEWER,
readStatus: ReadStatus.OPENED,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: null,
signingOrder: 1,
rejectionReason: null,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
Recipient: [
{
...basePayload.Recipient[0],
email: 'signer2@documenso.com',
name: 'Signer 2',
role: RecipientRole.VIEWER,
readStatus: ReadStatus.OPENED,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: null,
signingOrder: 1,
rejectionReason: null,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_SIGNED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.COMPLETED,
completedAt: now,
recipients: [
{
...basePayload.recipients[0],
id: 51,
email: 'signer1@documenso.com',
name: 'Signer 1',
token: 'SIGNING_TOKEN',
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signingOrder: 1,
rejectionReason: null,
},
],
Recipient: [
{
...basePayload.Recipient[0],
id: 51,
email: 'signer1@documenso.com',
name: 'Signer 1',
token: 'SIGNING_TOKEN',
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signingOrder: 1,
rejectionReason: null,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_COMPLETED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.COMPLETED,
completedAt: now,
recipients: [
{
id: 50,
documentId: 10,
templateId: null,
email: 'signer2@documenso.com',
name: 'Signer 2',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
signingOrder: 1,
rejectionReason: null,
role: RecipientRole.VIEWER,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
{
id: 51,
documentId: 10,
templateId: null,
email: 'signer1@documenso.com',
name: 'Signer 1',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
signingOrder: 2,
rejectionReason: null,
role: RecipientRole.SIGNER,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
],
Recipient: [
{
id: 50,
documentId: 10,
templateId: null,
email: 'signer2@documenso.com',
name: 'Signer 2',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
signingOrder: 1,
rejectionReason: null,
role: RecipientRole.VIEWER,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
{
id: 51,
documentId: 10,
templateId: null,
email: 'signer1@documenso.com',
name: 'Signer 1',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
signingOrder: 2,
rejectionReason: null,
role: RecipientRole.SIGNER,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_REJECTED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
rejectionReason: 'I do not agree with the terms',
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.REJECTED,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signingOrder: 1,
},
],
Recipient: [
{
...basePayload.Recipient[0],
signedAt: now,
authOptions: {
accessAuth: null,
actionAuth: null,
},
rejectionReason: 'I do not agree with the terms',
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.REJECTED,
sendStatus: SendStatus.SENT,
documentDeletedAt: null,
expired: null,
signingOrder: 1,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_CANCELLED) {
return {
event,
payload: {
...basePayload,
id: 7,
externalId: null,
userId: 3,
status: DocumentStatus.PENDING,
documentDataId: 'cm6exvn93006hi02ru90a265a',
documentMeta: {
...basePayload.documentMeta,
id: 'cm6exvn96006ji02rqvzjvwoy',
subject: '',
message: '',
timezone: 'Etc/UTC',
dateFormat: 'yyyy-MM-dd hh:mm a',
redirectUrl: '',
emailSettings: {
documentDeleted: true,
documentPending: true,
recipientSigned: true,
recipientRemoved: true,
documentCompleted: true,
ownerDocumentCompleted: true,
recipientSigningRequest: true,
},
},
recipients: [
{
id: 7,
documentId: 7,
templateId: null,
email: 'signer1@documenso.com',
name: 'Signer 1',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: {
accessAuth: null,
actionAuth: null,
},
signingOrder: 1,
rejectionReason: null,
role: RecipientRole.SIGNER,
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
sendStatus: SendStatus.SENT,
},
],
Recipient: [
{
id: 7,
documentId: 7,
templateId: null,
email: 'signer@documenso.com',
name: 'Signer',
token: 'SIGNING_TOKEN',
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: {
accessAuth: null,
actionAuth: null,
},
signingOrder: 1,
rejectionReason: null,
role: RecipientRole.SIGNER,
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
sendStatus: SendStatus.SENT,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
throw new Error(`Unsupported event type: ${event}`);
};

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"PO-Revision-Date: 2025-07-14 04:19\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -1690,7 +1690,7 @@ msgstr "Con copia"
#: packages/lib/constants/recipient-roles.ts
msgid "Ccers"
msgstr ""
msgstr "Firmantes"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
msgid "Character Limit"

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"PO-Revision-Date: 2025-07-14 04:19\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@ -2301,7 +2301,7 @@ msgstr "Struttura CSV"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Cumulative MAU (signed in)"
msgstr ""
msgstr "MAU cumulativi (autenticati)"
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
msgid "Current"
@ -4230,7 +4230,7 @@ msgstr "MAU (ha completato il documento)"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "MAU (signed in)"
msgstr ""
msgstr "MAU (autenticati)"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Max"

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: documenso-app\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-06-19 06:05\n"
"PO-Revision-Date: 2025-07-14 18: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"
@ -29,7 +29,7 @@ msgstr " Włącz podpisywanie linku bezpośredniego"
#: apps/remix/app/components/embed/authoring/configure-document-upload.tsx
msgid ".PDF documents accepted (max {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)"
msgstr "Akceptowane dokumenty .PDF (maks. {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB)"
msgstr "Akceptowane dokumenty .PDF (maks. {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB)"
#. placeholder {0}: field.customText
#. placeholder {1}: timezone || ''
@ -43,15 +43,15 @@ msgstr "Dokument \"{documentName}\" został usunięty przez administratora."
#: packages/email/template-components/template-document-pending.tsx
msgid "“{documentName}” has been signed"
msgstr "Dokument \"{documentName}\" został podpisany"
msgstr "Dokument {documentName} został podpisany"
#: packages/email/template-components/template-document-completed.tsx
msgid "“{documentName}” was signed by all signers"
msgstr "Dokument \"{documentName}\" został podpisany przez wszystkich podpisujących"
msgstr "Dokument {documentName} został podpisany przez wszystkich podpisujących"
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "\"{documentTitle}\" has been successfully deleted"
msgstr "Dokument \"{documentTitle}\" został usunięty"
msgstr "Dokument {documentTitle} został usunięty"
#: apps/remix/app/components/forms/document-preferences-form.tsx
msgid "\"{placeholderEmail}\" on behalf of \"Team Name\" has invited you to sign \"example document\"."
@ -254,7 +254,7 @@ msgstr "Użytkownik {prefix} otworzył dokument"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} prefilled a field"
msgstr "{prefix} wstępnie wypełnił pole"
msgstr "Użytkownik {prefix} wstępnie wypełnił pole"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} removed a field"
@ -304,7 +304,7 @@ msgstr "{prefix} zaktualizowane wymagania dotyczące autoryzacji dostępu do dok
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} updated the document external ID"
msgstr "{prefix} zaktualizowane ID zewnętrzne dokumentu"
msgstr "Użytkownik {prefix} zaktualizował identyfikator zewnętrzny dokumentu"
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} updated the document signing auth requirements"
@ -383,7 +383,7 @@ msgstr "{userName} dodał CC do dokumentu"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} completed their task"
msgstr "{userName} zakończył swoje zadanie"
msgstr "Użytkownik {userName} zakończył swoje zadanie"
#: packages/lib/utils/document-audit-logs.ts
msgid "{userName} rejected the document"
@ -802,7 +802,7 @@ msgstr "Dodaj wszystkie istotne pola dla każdego odbiorcy."
#: apps/remix/app/components/general/template/template-edit-form.tsx
msgid "Add all relevant placeholders for each recipient."
msgstr "Dodaj wszystkie odpowiednie symbole zastępcze dla każdego odbiorcy."
msgstr "Dodaj wszystkie odpowiednie teksty zastępcze dla każdego odbiorcy."
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Add an authenticator to serve as a secondary authentication method for signing documents."
@ -814,11 +814,11 @@ msgstr "Dodaj autoryzator, aby służył jako dodatkowa metoda uwierzytelniania
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Add an external ID to the document. This can be used to identify the document in external systems."
msgstr "Dodaj zewnętrzny ID do dokumentu. Może być używany do identyfikacji dokumentu w zewnętrznych systemach."
msgstr "Dodaj identyfikator zewnętrzny dokumentu. Może być używany do identyfikacji dokumentu w zewnętrznych systemach."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "Add an external ID to the template. This can be used to identify in external systems."
msgstr "Dodaj zewnętrzny ID do szablonu. Może być używany do identyfikacji w systemach zewnętrznych."
msgstr "Dodaj identyfikator zewnętrzny szablonu. Może być używany do identyfikacji w zewnętrznych systemach."
#: packages/ui/primitives/document-flow/field-items-advanced-settings/dropdown-field.tsx
msgid "Add another option"
@ -896,7 +896,7 @@ msgstr "Dodaj podpisujących"
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
msgid "Add signers and configure signing preferences"
msgstr "Dodaj sygnatariuszy i skonfiguruj preferencje podpisywania"
msgstr "Dodaj podpisujących i skonfiguruj ustawienia podpisywania"
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
msgid "Add team email"
@ -1025,7 +1025,7 @@ msgstr "Zezwól odbiorcom dokumentów na bezpośrednią odpowiedź na ten adres
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr "Pozwól sygnatariuszom wskazać następnego sygnatariusza"
msgstr "Zezwalaj podpisującym wskazać następnego podpisującego"
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
#: packages/ui/primitives/template-flow/add-template-settings.tsx
@ -1043,7 +1043,7 @@ msgstr "Pozwala na uwierzytelnianie za pomocą biometrii, menedżerów haseł, k
#: apps/remix/app/components/forms/signup.tsx
msgid "Already have an account? <0>Sign in instead</0>"
msgstr "Masz już konto? <0>Zaloguj się zamiast tego</0>"
msgstr "Masz już konto? <0>Zaloguj się</0>"
#: apps/remix/app/components/tables/organisation-billing-invoices-table.tsx
msgid "Amount"
@ -1582,7 +1582,7 @@ msgstr "Kontynuując z Twoim podpisem elektronicznym, przyjmujesz i zgadzasz si
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr "Kontynuując, zgadzasz się na nasze <0>Warunki korzystania z usługi</0> oraz <1>Politykę prywatności</1>."
msgstr "Kontynuując, akceptujesz nasz <0>Regulamin</0> i <1>Politykę prywatności</1>."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
@ -1673,7 +1673,7 @@ msgstr "Nie można usunąć dokumentu"
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Cannot remove signer"
msgstr "Nie można usunąć sygnatariusza"
msgstr "Nie można usunąć podpisującego"
#: packages/lib/constants/recipient-roles.ts
msgid "Cc"
@ -2257,7 +2257,7 @@ msgstr "Utwórz swoje konto i zacznij korzystać z nowoczesnego podpisywania dok
#: apps/remix/app/components/forms/signup.tsx
msgid "Create your account and start using state-of-the-art document signing. Open and beautiful signing is within your grasp."
msgstr "Utwórz swoje konto i zacznij korzystać z nowoczesnego podpisywania dokumentów. Otwarty i piękny podpis jest w zasięgu ręki."
msgstr "Utwórz konto i zacznij korzystać z nowoczesnego podpisywania dokumentów. Otwarty i piękny podpis jest w zasięgu ręki."
#: apps/remix/app/routes/_authenticated+/settings+/security.sessions.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
@ -2676,7 +2676,7 @@ msgstr "Dokument anulowany"
#: packages/lib/utils/document-audit-logs.ts
#: packages/lib/utils/document-audit-logs.ts
msgid "Document completed"
msgstr "Dokument ukończony"
msgstr "Zakończono dokument"
#: packages/ui/components/document/document-email-checkboxes.tsx
#: packages/ui/components/document/document-email-checkboxes.tsx
@ -2719,7 +2719,7 @@ msgstr "Tworzenie dokumentu"
#: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx
#: packages/lib/utils/document-audit-logs.ts
msgid "Document deleted"
msgstr "Dokument usunięty"
msgstr "Usunięto dokument"
#: packages/ui/components/document/document-email-checkboxes.tsx
msgid "Document deleted email"
@ -2744,7 +2744,7 @@ msgstr "Dokument zduplikowany"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document external ID updated"
msgstr "Zaktualizowane ID zewnętrzne dokumentu"
msgstr "Zaktualizowano identyfikator zewnętrzny dokumentu"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Document found in your account"
@ -2786,7 +2786,7 @@ msgstr "Dokument przeniesiony"
#: packages/lib/utils/document-audit-logs.ts
msgid "Document moved to team"
msgstr "Dokument przeniesiony do zespołu"
msgstr "Przeniesiono dokument do zespołu"
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
msgid "Document no longer available to sign"
@ -2959,7 +2959,7 @@ msgstr "Szkic"
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "Draft documents"
msgstr "Dokumenty szkiców"
msgstr "Szkice dokumentów"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Drafted Documents"
@ -3085,7 +3085,7 @@ msgstr "Adres e-mail"
#: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
msgid "Email already confirmed"
msgstr "E-mail już potwierdzony"
msgstr "Adres e-mail został już potwierdzony"
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
msgid "Email cannot already exist in the template"
@ -3093,11 +3093,11 @@ msgstr "E-mail nie może już istnieć w szablonie"
#: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
msgid "Email Confirmed!"
msgstr "E-mail potwierdzony!"
msgstr "Adres e-mail został potwierdzony!"
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "Email Options"
msgstr "Opcje e-mail"
msgstr "Opcje adresu e-mail"
#: packages/lib/utils/document-audit-logs.ts
msgid "Email resent"
@ -3109,7 +3109,7 @@ msgstr "Wysłano wiadomość"
#: apps/remix/app/routes/_unauthenticated+/check-email.tsx
msgid "Email sent!"
msgstr "E-mail wysłany!"
msgstr "Wiadomość została wysłana!"
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
msgid "Email verification has been removed"
@ -3209,7 +3209,7 @@ msgstr "Wprowadź szczegóły swojej marki"
#: apps/remix/app/components/general/claim-account.tsx
msgid "Enter your email"
msgstr "Wprowadź swój adres e-mail"
msgstr "Wpisz adres e-mail"
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
msgid "Enter your email address to receive the completed document."
@ -3217,7 +3217,7 @@ msgstr "Wprowadź swój adres e-mail, aby otrzymać ukończony dokument."
#: apps/remix/app/components/general/claim-account.tsx
msgid "Enter your name"
msgstr "Wprowadź swoje imię"
msgstr "Wpisz nazwę"
#: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx
msgid "Enter your password"
@ -3321,7 +3321,7 @@ msgstr "Wygasa {0}"
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "External ID"
msgstr "Zewnętrzny ID"
msgstr "Identyfikator zewnętrzny"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Failed to create folder"
@ -3333,7 +3333,7 @@ msgstr "Nie udało się utworzyć roszczenia subskrypcyjnego."
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Failed to delete folder"
msgstr "Nie udało się usunąć folder"
msgstr "Nie udało się usunąć folderu"
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
msgid "Failed to delete subscription claim."
@ -3365,7 +3365,7 @@ msgstr "Nie udało się wylogować ze wszystkich sesji"
#: apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx
msgid "Failed to update document"
msgstr "Nie udało się aktualizować dokumentu"
msgstr "Nie udało się zaktualizować dokumentu"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Failed to update recipient"
@ -3385,7 +3385,7 @@ msgstr "Nie udało się zaktualizować webhooku"
#: packages/email/templates/bulk-send-complete.tsx
msgid "Failed: {failedCount}"
msgstr "Niepowodzenia: {failedCount}"
msgstr "Niepowodzenie: {failedCount}"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/components/tables/admin-claims-table.tsx
@ -3418,11 +3418,11 @@ msgstr "Etykieta pola"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx
msgid "Field placeholder"
msgstr "Zastępczy tekst pola"
msgstr "Tekst zastępczy pola"
#: packages/lib/utils/document-audit-logs.ts
msgid "Field prefilled by assistant"
msgstr "Pole wstępnie wypełnione przez asystenta"
msgstr "Wstępnie wypełniono pole przez asystenta"
#: packages/lib/utils/document-audit-logs.ts
msgid "Field signed"
@ -3461,19 +3461,19 @@ msgstr "Folder"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder created successfully"
msgstr "Folder utworzony pomyślnie"
msgstr "Folder został utworzony"
#: apps/remix/app/components/dialogs/folder-delete-dialog.tsx
msgid "Folder deleted successfully"
msgstr "Folder został pomyślnie usunięty"
msgstr "Folder został usunięty"
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
msgid "Folder moved successfully"
msgstr "Folder został pomyślnie przeniesiony"
msgstr "Folder został przeniesiony"
#: apps/remix/app/components/dialogs/folder-create-dialog.tsx
msgid "Folder Name"
msgstr "Nazwa foldera"
msgstr "Nazwa folderu"
#: apps/remix/app/components/dialogs/folder-settings-dialog.tsx
#: apps/remix/app/components/dialogs/folder-move-dialog.tsx
@ -3522,7 +3522,7 @@ msgstr "Darmowy"
#: packages/ui/primitives/document-flow/types.ts
msgid "Free Signature"
msgstr "Podpis wolny"
msgstr "Swobodny podpis"
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
@ -3676,7 +3676,7 @@ msgstr "Asystent jako ostatni sygnatariusz oznacza, że nie będą oni mogli pod
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
msgid "Help complete the document for other signers."
msgstr "Pomóc zakończyć dokument dla innych sygnatariuszy."
msgstr "Pomóż zakończyć dokument dla innych podpisujących."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.general.tsx
msgid "Here you can edit your organisation details."
@ -3742,11 +3742,11 @@ msgstr "Dom"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
#: apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
msgid "Home (No Folder)"
msgstr "Strona główna (Brak folderu)"
msgstr "Strona główna (brak folderu)"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a signer of this document"
msgstr "Jestem sygnatariuszem tego dokumentu"
msgstr "Jestem podpisującym tego dokumentu"
#: packages/lib/constants/recipient-roles.ts
msgid "I am a viewer of this document"
@ -3775,11 +3775,11 @@ msgstr "Jestem pewny! Usuń to"
#: apps/remix/app/components/tables/admin-claims-table.tsx
msgid "ID"
msgstr "ID"
msgstr "Identyfikator"
#: apps/remix/app/components/tables/admin-claims-table.tsx
msgid "ID copied to clipboard"
msgstr "ID skopiowane do schowka"
msgstr "Identyfikator został skopiowany do schowka"
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "If you do not want to use the authenticator prompted, you can close it, which will then display the next available authenticator."
@ -3854,16 +3854,16 @@ msgstr "Nieprawidłowy kod. Proszę spróbuj ponownie."
#: packages/ui/primitives/document-flow/add-signers.types.ts
msgid "Invalid email"
msgstr "Nieprawidłowy email"
msgstr "Adres e-mail jest nieprawidłowy"
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Invalid link"
msgstr "Nieprawidłowy link"
msgstr "Link jest nieprawidłowy"
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
msgid "Invalid token"
msgstr "Nieprawidłowy token"
msgstr "Token jest nieprawidłowy"
#: apps/remix/app/components/forms/reset-password.tsx
msgid "Invalid token provided. Please try again."
@ -3871,16 +3871,16 @@ msgstr "Podano nieprawidłowy token. Spróbuj ponownie."
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
msgid "Invitation accepted"
msgstr "Zaproszenie zaakceptowane"
msgstr "Zaproszenie został zaakceptowane"
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
msgid "Invitation accepted!"
msgstr "Zaproszenie zaakceptowane!"
msgstr "Zaproszenie zostało zaakceptowane!"
#: apps/remix/app/routes/_unauthenticated+/organisation.decline.$token.tsx
#: apps/remix/app/components/general/organisations/organisation-invitations.tsx
msgid "Invitation declined"
msgstr "Zaproszenie odrzucone"
msgstr "Zaproszenie zzostało odrzucone"
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
msgid "Invitation has been deleted"
@ -4703,11 +4703,11 @@ msgstr "Administrator organizacji"
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
msgid "Organisation created"
msgstr "Organizacja utworzona"
msgstr "Organizacja została utworzona"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
msgid "Organisation group not found"
msgstr "Grupa organizacji nie znaleziona"
msgstr "Grupa organizacji nie została znaleziona"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
msgid "Organisation Group Settings"
@ -4716,7 +4716,7 @@ msgstr "Ustawienia grupy organizacji"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Organisation has been updated successfully"
msgstr "Organizacja zaktualizowana pomyślnie"
msgstr "Organizacja została zaktualizowana"
#: apps/remix/app/routes/_unauthenticated+/organisation.invite.$token.tsx
msgid "Organisation invitation"
@ -4756,16 +4756,16 @@ msgstr "Organizacja nie znaleziona"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.preferences.tsx
msgid "Organisation Preferences"
msgstr "Preferencje organizacji"
msgstr "Ustawienia organizacji"
#: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx
msgid "Organisation role"
msgstr "Rola organizacyjna"
msgstr "Rola w organizacji"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
#: apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx
msgid "Organisation Role"
msgstr "Rola organizacyjna"
msgstr "Rola w organizacji"
#: apps/remix/app/components/general/org-menu-switcher.tsx
msgid "Organisation settings"
@ -4777,12 +4777,12 @@ msgstr "Ustawienia organizacji"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Organisation Teams"
msgstr "Zespoły organizacyjne"
msgstr "Zespoły w organizacji"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#: apps/remix/app/components/forms/organisation-update-form.tsx
msgid "Organisation URL"
msgstr "URL organizacji"
msgstr "Adres URL organizacji"
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
#: apps/remix/app/routes/_authenticated+/settings+/organisations.tsx
@ -4914,7 +4914,7 @@ msgstr "Hasło zostało zaktualizowane"
#: packages/email/template-components/template-reset-password.tsx
msgid "Password updated!"
msgstr "Hasło zaktualizowane!"
msgstr "Hasło zostało zaktualizowane!"
#: packages/ui/primitives/data-table/data/data.tsx
msgid "Past"
@ -4958,7 +4958,7 @@ msgstr "miesięcznie"
#: apps/remix/app/components/general/billing-plans.tsx
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "per year"
msgstr "na rok"
msgstr "rocznie"
#: apps/remix/app/components/tables/user-organisations-table.tsx
msgid "Personal"
@ -4986,7 +4986,7 @@ msgstr "Wybierz hasło"
#: apps/remix/app/components/general/user-profile-timur.tsx
msgid "Pick any of the following agreements below and start signing to get started"
msgstr "Wybierz dowolną z poniższych umów i zacznij podpisywanie, aby rozpocząć"
msgstr "Wybierz umowę i zacznij podpisywanie"
#: apps/remix/app/components/general/folder/folder-card.tsx
msgid "Pin"
@ -4996,7 +4996,7 @@ msgstr "Przypnij"
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
#: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx
msgid "Placeholder"
msgstr "Zastępczy tekst"
msgstr "Tekst zastępczy"
#. placeholder {0}: _(actionVerb).toLowerCase()
#: packages/email/template-components/template-document-invite.tsx
@ -5042,11 +5042,11 @@ msgstr "Najpierw skonfiguruj dokument"
#: packages/lib/server-only/auth/send-confirmation-email.ts
msgid "Please confirm your email"
msgstr "Proszę potwierdzić swój email"
msgstr "Potwierdź adres e-mail"
#: packages/email/templates/confirm-email.tsx
msgid "Please confirm your email address"
msgstr "Proszę potwierdzić swój adres email"
msgstr "Potwierdź adres e-mail"
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "Please contact support if you would like to revert this action."
@ -5064,7 +5064,7 @@ msgstr "Wpisz nazwę tokena. Pomoże to później w jego identyfikacji."
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Proszę wpisać poprawną nazwę."
msgstr "Wpisz prawdłową nazwę."
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
msgid "Please mark as viewed to complete"
@ -5116,7 +5116,7 @@ msgstr "Proszę przejrzeć dokument przed podpisaniem."
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
msgid "Please select a PDF file"
msgstr "Proszę wybrać plik PDF"
msgstr "Wybierz plik PDF"
#: apps/remix/app/components/forms/send-confirmation-email.tsx
msgid "Please try again and make sure you enter the correct email address."
@ -5124,7 +5124,7 @@ msgstr "Spróbuj ponownie i upewnij się, że wprowadzasz poprawny adres email."
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
msgid "Please try again later."
msgstr "Proszę spróbować ponownie później."
msgstr "Spróbuj ponownie później."
#: packages/ui/primitives/pdf-viewer.tsx
#: packages/ui/primitives/pdf-viewer.tsx
@ -5157,7 +5157,7 @@ msgstr "Wstępnie sformatowany szablon CSV z przykładowymi danymi."
#: apps/remix/app/components/general/teams/team-settings-nav-mobile.tsx
#: apps/remix/app/components/general/teams/team-settings-nav-desktop.tsx
msgid "Preferences"
msgstr "Preferencje"
msgstr "Ustawienia"
#: packages/ui/primitives/data-table/data/data.tsx
msgid "Present"
@ -5323,7 +5323,7 @@ msgstr "E-mail z prośbą o podpisanie przez odbiorcę"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
msgid "Recipient updated"
msgstr "Odbiorca zaktualizowany"
msgstr "Odbiorca został zaktualizowany"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
@ -5343,7 +5343,7 @@ msgstr "Odbiorcy nadal zachowają swoją kopię dokumentu"
#: apps/remix/app/components/forms/2fa/recovery-code-list.tsx
msgid "Recovery code copied"
msgstr "Kod odzyskiwania skopiowany"
msgstr "Kod odzyskiwania został skopiowany"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Recovery codes"
@ -5379,7 +5379,7 @@ msgstr "Odrzuć dokument"
#: packages/ui/primitives/data-table/data/data.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Odrzucony"
msgstr "Odrzucono"
#: packages/email/template-components/template-document-rejection-confirmed.tsx
msgid "Rejection Confirmed"
@ -6025,7 +6025,7 @@ msgstr "Identyfikator podpisu"
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr "Podpis jest za mały"
msgstr "Podpis jest zbyt mały"
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
@ -6048,15 +6048,15 @@ msgstr "Podpisy pojawią się po ukończeniu dokumentu"
#: packages/ui/components/document/document-read-only-fields.tsx
#: packages/lib/constants/recipient-roles.ts
msgid "Signed"
msgstr "Podpisano"
msgstr "Podpisał"
#: packages/lib/constants/recipient-roles.ts
msgid "Signer"
msgstr "Sygnatariusz"
msgstr "Podpisujący"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
msgid "Signer Events"
msgstr "Wydarzenia sygnatariusza"
msgstr "Zdarzenia podpisujących"
#: packages/lib/constants/recipient-roles.ts
msgid "Signers"
@ -6262,17 +6262,17 @@ msgstr "Stripe"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Stripe customer created successfully"
msgstr "Klient Stripe utworzony pomyślnie"
msgstr "Klient Stripe został utworzony"
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
msgid "Stripe Customer ID"
msgstr "ID klienta Stripe"
msgstr "Identyfikator klienta Stripe"
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Subject <0>(Optional)</0>"
msgstr "Temat <0>(Opcjonalnie)</0>"
msgstr "Temat <0>(opcjonalnie)</0>"
#: apps/remix/app/components/general/billing-plans.tsx
#: apps/remix/app/components/general/billing-plans.tsx
@ -6395,19 +6395,19 @@ msgstr "Liczba zespołów"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx
msgid "Team email"
msgstr "E-mail zespołu"
msgstr "Adres e-mail zespołu"
#: apps/remix/app/components/general/teams/team-email-usage.tsx
msgid "Team Email"
msgstr "E-mail zespołu"
msgstr "Adres e-mail zespołu"
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Team email already verified!"
msgstr "E-mail zespołu został już zweryfikowany!"
msgstr "Adres e-mail zespołu został już zweryfikowany!"
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
msgid "Team email has been removed"
msgstr "E-mail zespołu został usunięty"
msgstr "Adres e-mail zespołu został usunięty"
#. placeholder {0}: team.name
#: packages/lib/server-only/team/delete-team-email.ts
@ -6416,7 +6416,7 @@ msgstr "Email zespołowy został anulowany dla {0}"
#: packages/email/templates/team-email-removed.tsx
msgid "Team email removed"
msgstr "Email zespołowy usunięty"
msgstr "Adres e-mail zespołu usunięty"
#: packages/email/templates/team-email-removed.tsx
msgid "Team email removed for {teamName} on Documenso"
@ -6424,15 +6424,15 @@ msgstr "Email zespołowy usunięty dla {teamName} na Documenso"
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Team email verification"
msgstr "Weryfikacja e-maila zespołu"
msgstr "Weryfikacja adresu e-mail zespołu"
#: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx
msgid "Team email verified!"
msgstr "E-mail zespołu zweryfikowany!"
msgstr "Adres e-mail zespołu został zweryfikowany!"
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
msgid "Team email was updated."
msgstr "E-mail zespołu został zaktualizowany."
msgstr "Adres e-mail zespołu został zaktualizowany."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.groups.tsx
msgid "Team Groups"
@ -6476,7 +6476,7 @@ msgstr "Szablony tylko dla zespołu nie są nigdzie linkowane i są widoczne tyl
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.preferences.tsx
msgid "Team Preferences"
msgstr "Preferencje zespołu"
msgstr "Ustawienia zespołu"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
@ -6528,19 +6528,19 @@ msgstr "Szablon"
#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx
msgid "Template Created"
msgstr "Szablon utworzony"
msgstr "Szablon został utworzony"
#: apps/remix/app/components/dialogs/template-delete-dialog.tsx
msgid "Template deleted"
msgstr "Szablon usunięty"
msgstr "Szablon został usunięty"
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
msgid "Template document uploaded"
msgstr "Dokument szablonu przesłany"
msgstr "Szablon dokumentu został przesłany"
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
msgid "Template duplicated"
msgstr "Szablon skopiowany"
msgstr "Szablon został zduplikowany"
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
msgid "Template has been removed from your public profile."
@ -6556,11 +6556,11 @@ msgstr "Szablon używa starej metody wstawiania pól"
#: apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx
msgid "Template moved"
msgstr "Szablon przeniesiony"
msgstr "Szablon został przeniesiony"
#: apps/remix/app/components/general/template/template-edit-form.tsx
msgid "Template saved"
msgstr "Szablon zapisany"
msgstr "Szablon został zapisany"
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "Template title"
@ -6568,7 +6568,7 @@ msgstr "Tytuł szablonu"
#: apps/remix/app/routes/embed+/v1+/authoring+/template.edit.$id.tsx
msgid "Template updated successfully"
msgstr "Szablon zaktualizowany pomyślnie"
msgstr "Szablon został zaktualizowany"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@ -6618,15 +6618,15 @@ msgstr "To w porządku, zdarza się! Kliknij przycisk poniżej, aby zresetować
#: apps/remix/app/components/dialogs/admin-user-delete-dialog.tsx
msgid "The account has been deleted successfully."
msgstr "Konto zostało pomyślnie usunięte."
msgstr "Konto zostało usunięte."
#: apps/remix/app/components/dialogs/admin-user-disable-dialog.tsx
msgid "The account has been disabled successfully."
msgstr "Konto zostało pomyślnie wyłączone."
msgstr "Konto zostało wyłączone."
#: apps/remix/app/components/dialogs/admin-user-enable-dialog.tsx
msgid "The account has been enabled successfully."
msgstr "Konto zostało pomyślnie włączone."
msgstr "Konto zostało włączone."
#: packages/ui/components/recipient/recipient-action-auth-select.tsx
msgid "The authentication methods required for recipients to sign fields"
@ -6745,7 +6745,7 @@ msgstr "Organizacja, której szukasz, mogła zostać usunięta, zmieniona nazwa
#: apps/remix/app/components/general/generic-error-layout.tsx
msgid "The page you are looking for was moved, removed, renamed or might never have existed."
msgstr "Strona, której szukasz, została przeniesiona, usunięta, zmieniona lub mogła nigdy nie istnieć."
msgstr "Strona została przeniesiona, usunięta, zmieniona lub mogła nie istnieć."
#: apps/remix/app/components/forms/public-profile-form.tsx
msgid "The profile link has been copied to your clipboard"
@ -7255,7 +7255,7 @@ msgstr "Token został utworzony"
#: apps/remix/app/components/dialogs/token-delete-dialog.tsx
msgid "Token deleted"
msgstr "Token usunięty"
msgstr "Token został usunięty"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
msgid "Token doesn't have an expiration date"
@ -7267,7 +7267,7 @@ msgstr "Data wygaśnięcia tokena"
#: apps/remix/app/components/forms/reset-password.tsx
msgid "Token has expired. Please try again."
msgstr "Token wygasł. Proszę spróbować ponownie."
msgstr "Token wygasł. Spróbuj ponownie."
#: apps/remix/app/components/forms/token.tsx
msgid "Token name"
@ -7460,7 +7460,7 @@ msgstr "Odepnij"
#: apps/remix/app/components/dialogs/team-group-create-dialog.tsx
msgid "Untitled Group"
msgstr "Grupa bez tytułu"
msgstr "Grupa bez nazwy"
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
@ -7545,7 +7545,7 @@ msgstr "Zaktualizuj zespół"
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
msgid "Update team email"
msgstr "Zaktualizuj e-mail zespołu"
msgstr "Zaktualizuj adres e-mail zespołu"
#: apps/remix/app/components/dialogs/team-group-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-group-update-dialog.tsx
@ -7575,7 +7575,7 @@ msgstr "Aktualizowanie hasła..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Aktualizacja Twoich informacji"
msgstr "Aktualizowanie informacji"
#: packages/ui/primitives/document-upload.tsx
#: packages/ui/primitives/document-dropzone.tsx
@ -7585,7 +7585,7 @@ msgstr "Ulepsz"
#. placeholder {0}: organisation.name
#: apps/remix/app/components/general/billing-plans.tsx
msgid "Upgrade <0>{0}</0> to {planName}"
msgstr "Uaktualnij <0>{0}</0> do {planName}"
msgstr "Ulepsz <0>{0}</0> do {planName}"
#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
msgid "Upgrade your plan to upload more documents"
@ -7632,11 +7632,11 @@ msgstr "Prześlij podpis"
#: packages/ui/primitives/document-upload.tsx
#: packages/ui/primitives/document-dropzone.tsx
msgid "Upload Template Document"
msgstr "Prześlij dokument szablonu"
msgstr "Prześlij szablon dokumentu"
#: apps/remix/app/components/forms/branding-preferences-form.tsx
msgid "Upload your brand logo (max 5MB, JPG, PNG, or WebP)"
msgstr "Prześlij logo swojej marki (maks. 5MB, JPG, PNG lub WebP)"
msgstr "Prześlij swoje logo (maks. 5 MB, JPG, PNG lub WebP)"
#: apps/remix/app/components/general/template/template-page-view-information.tsx
#: apps/remix/app/components/general/document/document-page-view-information.tsx
@ -7745,11 +7745,11 @@ msgstr "Zweryfikuj teraz"
#: apps/remix/app/components/general/verify-email-banner.tsx
msgid "Verify your email address"
msgstr "Zweryfikuj swój adres e-mail"
msgstr "Zweryfikuj adres e-mail"
#: apps/remix/app/components/general/verify-email-banner.tsx
msgid "Verify your email address to unlock all features."
msgstr "Zweryfikuj swój adres e-mail, aby odblokować wszystkie funkcje."
msgstr "Zweryfikuj adres e-mail, aby odblokować wszystkie funkcje."
#: apps/remix/app/components/general/document/document-upload.tsx
msgid "Verify your email to upload documents."
@ -7757,7 +7757,7 @@ msgstr "Zweryfikuj adres e-mail, aby przesłać dokumenty."
#: packages/email/templates/confirm-team-email.tsx
msgid "Verify your team email address"
msgstr "Zweryfikuj swój adres e-mail zespołu"
msgstr "Zweryfikuj adres e-mail zespołu"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx
msgid "Version History"
@ -7864,7 +7864,7 @@ msgstr "Wyświetl zespoły"
#: apps/remix/app/components/embed/multisign/multi-sign-document-list.tsx
#: packages/lib/constants/recipient-roles.ts
msgid "Viewed"
msgstr "Wyświetlono"
msgstr "Wyświetl"
#: packages/lib/constants/recipient-roles.ts
msgid "Viewer"
@ -8210,7 +8210,7 @@ msgstr "Witaj"
#: apps/remix/app/routes/_unauthenticated+/signin.tsx
msgid "Welcome back, we are lucky to have you."
msgstr "Witamy z powrotem, mamy szczęście, że mamy cię."
msgstr "Witaj ponownie. Mamy szczęście, że Cię mamy."
#: apps/remix/app/routes/_authenticated+/dashboard.tsx
msgid "Welcome back! Here's an overview of your account."
@ -8716,7 +8716,7 @@ msgstr "Twój dokument został pomyślnie utworzony na podstawie szablonu."
#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx
msgid "Your document has been created successfully"
msgstr "Twój dokument został pomyślnie utworzony"
msgstr "Dokument został utworzony"
#: packages/email/template-components/template-document-super-delete.tsx
msgid "Your document has been deleted by an admin!"
@ -8728,25 +8728,25 @@ msgstr "Twój dokument został pomyślnie ponownie wysłany."
#: apps/remix/app/components/general/document/document-edit-form.tsx
msgid "Your document has been sent successfully."
msgstr "Twój dokument został pomyślnie wysłany."
msgstr "Dokument został wysłany."
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
msgid "Your document has been successfully duplicated."
msgstr "Twój dokument został pomyślnie zduplikowany."
msgstr "Dokument został zduplikowany."
#: apps/remix/app/components/general/document/document-upload.tsx
#: apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx
msgid "Your document has been uploaded successfully."
msgstr "Twój dokument został pomyślnie załadowany."
msgstr "Dokument został przesłany."
#: apps/remix/app/components/dialogs/template-create-dialog.tsx
msgid "Your document has been uploaded successfully. You will be redirected to the template page."
msgstr "Twój dokument został pomyślnie załadowany. Zostaniesz przekierowany na stronę szablonu."
msgstr "Dokument został przesłany. Zostaniesz przekierowany na stronę szablonu."
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.preferences.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.preferences.tsx
msgid "Your document preferences have been updated"
msgstr "Preferencje dokumentu zostały zaktualizowane"
msgstr "Ustawienia dokumentu zostały zaktualizowane"
#: apps/remix/app/components/general/app-command-menu.tsx
msgid "Your documents"
@ -8754,11 +8754,11 @@ msgstr "Twoje dokumenty"
#: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
msgid "Your email has already been confirmed. You can now use all features of Documenso."
msgstr "Twój e-mail został już potwierdzony. Możesz teraz korzystać z wszystkich funkcji Documenso."
msgstr "Adres e-mail został już potwierdzony. Możesz korzystać ze wszystkich funkcji Documenso."
#: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
msgid "Your email has been successfully confirmed! You can now use all features of Documenso."
msgstr "Twój adres e-mail został pomyślnie potwierdzony! Możesz teraz korzystać ze wszystkich funkcji Documenso."
msgstr "Adres e-mail został potwierdzony! Możesz korzystać ze wszystkich funkcji Documenso."
#. placeholder {0}: teamEmail.team.name
#. placeholder {1}: teamEmail.team.url
@ -8781,15 +8781,15 @@ msgstr "Twoje nowe hasło nie może być takie samo jak stare hasło."
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
msgid "Your organisation has been created."
msgstr "Twoja organizacja została utworzona."
msgstr "Organizacja została utworzona."
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
msgid "Your organisation has been successfully deleted."
msgstr "Twoja organizacja została pomyślnie usunięta."
msgstr "Organizacja została usunięta."
#: apps/remix/app/components/forms/organisation-update-form.tsx
msgid "Your organisation has been successfully updated."
msgstr "Twoja organizacja została pomyślnie zaktualizowana."
msgstr "Organizacja została zaktualizowana."
#: apps/remix/app/components/forms/reset-password.tsx
#: apps/remix/app/components/forms/password.tsx
@ -8814,11 +8814,11 @@ msgstr "Twój plan nie wspiera zapraszania członków. Zaktualizuj swój plan lu
#: apps/remix/app/components/forms/profile.tsx
msgid "Your profile has been updated successfully."
msgstr "Twój profil został pomyślnie zaktualizowany."
msgstr "Profil został zaktualizowany."
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
msgid "Your profile has been updated."
msgstr "Twój profil został zaktualizowany."
msgstr "Profil został zaktualizowany."
#: apps/remix/app/components/forms/public-profile-form.tsx
msgid "Your public profile has been updated."
@ -8835,35 +8835,35 @@ msgstr "Twoje kody odzyskiwania są wymienione poniżej. Proszę przechowywać j
#: apps/remix/app/components/dialogs/team-create-dialog.tsx
msgid "Your team has been created."
msgstr "Twój zespół został utworzony."
msgstr "Zespół został utworzony."
#: apps/remix/app/components/dialogs/team-delete-dialog.tsx
msgid "Your team has been successfully deleted."
msgstr "Twój zespół został pomyślnie usunięty."
msgstr "Zespół został usunięty."
#: apps/remix/app/components/forms/team-update-form.tsx
msgid "Your team has been successfully updated."
msgstr "Twój zespół został pomyślnie zaktualizowany."
msgstr "Zespół został zaktualizowany."
#: apps/remix/app/routes/embed+/v1+/authoring_.completed.create.tsx
msgid "Your template has been created successfully"
msgstr "Twój szablon został pomyślnie utworzony"
msgstr "Szablon został utworzony"
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
msgid "Your template has been duplicated successfully."
msgstr "Twój szablon został pomyślnie zduplikowany."
msgstr "Szablon został zduplikowany."
#: apps/remix/app/components/dialogs/template-delete-dialog.tsx
msgid "Your template has been successfully deleted."
msgstr "Twój szablon został pomyślnie usunięty."
msgstr "Szablon został usunięty."
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
msgid "Your template will be duplicated."
msgstr "Twój szablon zostanie zduplikowany."
msgstr "Szablon zostanie zduplikowany."
#: apps/remix/app/components/general/template/template-edit-form.tsx
msgid "Your templates has been saved successfully."
msgstr "Twoje szablony zostały pomyślnie zapisane."
msgstr "Szablony zostały zapisane."
#: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx
msgid "Your token has expired!"

View File

@ -96,6 +96,7 @@ export const ZCheckboxFieldMeta = ZBaseFieldMeta.extend({
.optional(),
validationRule: z.string().optional(),
validationLength: z.number().optional(),
direction: z.enum(['vertical', 'horizontal']).optional().default('vertical'),
});
export type TCheckboxFieldMeta = z.infer<typeof ZCheckboxFieldMeta>;

View File

@ -1,4 +1,4 @@
import type { Document, DocumentMeta, Recipient } from '@prisma/client';
import type { Document, DocumentMeta, Recipient, WebhookTriggerEvents } from '@prisma/client';
import {
DocumentDistributionMethod,
DocumentSigningOrder,
@ -87,6 +87,13 @@ export const ZWebhookDocumentSchema = z.object({
export type TWebhookRecipient = z.infer<typeof ZWebhookRecipientSchema>;
export type TWebhookDocument = z.infer<typeof ZWebhookDocumentSchema>;
export type WebhookPayload = {
event: WebhookTriggerEvents;
payload: TWebhookDocument;
createdAt: string;
webhookEndpoint: string;
};
export const mapDocumentToWebhookDocumentPayload = (
document: Document & {
recipients: Recipient[];

View File

@ -0,0 +1,11 @@
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_folderId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_folderId_fkey";
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -397,7 +397,7 @@ model Document {
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
auditLogs DocumentAuditLog[]
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId String?
@@unique([documentDataId])
@ -866,7 +866,7 @@ model Template {
directLink TemplateDirectLink?
documents Document[]
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId String?
@@unique([templateDocumentDataId])

View File

@ -207,7 +207,9 @@ export const seedTeamTemplateWithMeta = async (team: Team) => {
const ownerUser = organisation.owner;
const template = await createTemplate({
title: `[TEST] Template ${nanoid(8)} - Draft`,
data: {
title: `[TEST] Template ${nanoid(8)} - Draft`,
},
userId: ownerUser.id,
teamId: team.id,
templateDocumentDataId: documentData.id,

View File

@ -12,7 +12,7 @@ import { ZCreateSubscriptionRequestSchema } from './create-subscription.types';
export const createSubscriptionRoute = authenticatedProcedure
.input(ZCreateSubscriptionRequestSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, priceId } = input;
const { organisationId, priceId, isPersonalLayoutMode } = input;
ctx.logger.info({
input: {
@ -70,10 +70,14 @@ export const createSubscriptionRoute = authenticatedProcedure
});
}
const returnUrl = isPersonalLayoutMode
? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`
: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`;
const redirectUrl = await createCheckoutSession({
customerId,
priceId,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`,
returnUrl,
});
if (!redirectUrl) {

View File

@ -3,4 +3,5 @@ import { z } from 'zod';
export const ZCreateSubscriptionRequestSchema = z.object({
organisationId: z.string().describe('The organisation to create the subscription for'),
priceId: z.string().describe('The price to create the subscription for'),
isPersonalLayoutMode: z.boolean().optional(),
});

View File

@ -322,7 +322,7 @@ export const documentRouter = router({
return {
document: createdDocument,
folder: createdDocument.folder,
folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
uploadUrl: url,
};
}),

View File

@ -33,7 +33,9 @@ export const createEmbeddingTemplateRoute = procedure
// First create the template
const template = await createTemplate({
userId: apiToken.userId,
title,
data: {
title,
},
templateDocumentDataId: documentDataId,
teamId: apiToken.teamId ?? undefined,
});

View File

@ -1,9 +1,11 @@
import type { Document } from '@prisma/client';
import { DocumentDataType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import {
@ -23,6 +25,7 @@ import { findTemplates } from '@documenso/lib/server-only/template/find-template
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
@ -34,6 +37,8 @@ import {
ZCreateTemplateDirectLinkRequestSchema,
ZCreateTemplateDirectLinkResponseSchema,
ZCreateTemplateMutationSchema,
ZCreateTemplateV2RequestSchema,
ZCreateTemplateV2ResponseSchema,
ZDeleteTemplateDirectLinkRequestSchema,
ZDeleteTemplateMutationSchema,
ZDuplicateTemplateMutationSchema,
@ -141,12 +146,88 @@ export const templateRouter = router({
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
folderId,
data: {
title,
folderId,
},
});
}),
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
* @deprecated
*/
createTemplateTemporary: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/create/beta',
summary: 'Create template',
description:
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
tags: ['Template'],
},
})
.input(ZCreateTemplateV2RequestSchema)
.output(ZCreateTemplateV2ResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
meta,
} = input;
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const templateDocumentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const createdTemplate = await createTemplate({
userId: user.id,
teamId,
templateDocumentDataId: templateDocumentData.id,
data: {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
},
meta,
});
const fullTemplate = await getTemplateById({
id: createdTemplate.id,
userId: user.id,
teamId,
});
return {
template: fullTemplate,
uploadUrl: url,
};
}),
/**
* @public
*/

View File

@ -30,6 +30,50 @@ import {
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
export const ZTemplateTitleSchema = z
.string()
.trim()
.min(1)
.max(255)
.describe('The title of the document.');
export const ZTemplatePublicTitleSchema = z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
);
export const ZTemplatePublicDescriptionSchema = z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
);
export const ZTemplateMetaUpsertSchema = z.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
});
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
templateDocumentDataId: z.string().min(1),
@ -123,57 +167,46 @@ export const ZDeleteTemplateMutationSchema = z.object({
templateId: z.number(),
});
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
/**
* Note: This is the same between V1 and V2. Be careful when updating this schema and think of the consequences.
*/
export const ZCreateTemplateV2RequestSchema = z.object({
title: ZTemplateTitleSchema,
folderId: z.string().optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
publicTitle: ZTemplatePublicTitleSchema.optional(),
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
});
/**
* Note: This is the same between V1 and V2. Be careful when updating this schema and think of the consequences.
*/
export const ZCreateTemplateV2ResponseSchema = z.object({
template: ZTemplateSchema,
uploadUrl: z.string().min(1),
});
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
data: z
.object({
title: z.string().min(1).optional(),
title: ZTemplateTitleSchema.optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
publicTitle: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicTitle: ZTemplatePublicTitleSchema.optional(),
publicDescription: ZTemplatePublicDescriptionSchema.optional(),
type: z.nativeEnum(TemplateType).optional(),
useLegacyFieldInsertion: z.boolean().optional(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
})
.optional(),
meta: ZTemplateMetaUpsertSchema.optional(),
});
export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema;

View File

@ -3,6 +3,7 @@ import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-we
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
import { triggerTestWebhook } from '@documenso/lib/server-only/webhooks/trigger-test-webhook';
import { authenticatedProcedure, router } from '../trpc';
import {
@ -11,6 +12,7 @@ import {
ZEditWebhookRequestSchema,
ZGetTeamWebhooksRequestSchema,
ZGetWebhookByIdRequestSchema,
ZTriggerTestWebhookRequestSchema,
} from './schema';
export const webhookRouter = router({
@ -106,4 +108,25 @@ export const webhookRouter = router({
teamId,
});
}),
testWebhook: authenticatedProcedure
.input(ZTriggerTestWebhookRequestSchema)
.mutation(async ({ input, ctx }) => {
const { id, event, teamId } = input;
ctx.logger.info({
input: {
id,
event,
teamId,
},
});
return await triggerTestWebhook({
id,
event,
userId: ctx.user.id,
teamId,
});
}),
});

View File

@ -38,3 +38,11 @@ export const ZDeleteWebhookRequestSchema = z.object({
});
export type TDeleteWebhookRequestSchema = z.infer<typeof ZDeleteWebhookRequestSchema>;
export const ZTriggerTestWebhookRequestSchema = z.object({
id: z.string(),
event: z.nativeEnum(WebhookTriggerEvents),
teamId: z.number(),
});
export type TTriggerTestWebhookRequestSchema = z.infer<typeof ZTriggerTestWebhookRequestSchema>;

View File

@ -6,7 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
export const DocumentSignatureSettingsTooltip = () => {
return (
<Tooltip>
<TooltipTrigger>
<TooltipTrigger type="button">
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>

View File

@ -3,7 +3,9 @@ import React, { useEffect, useMemo, useState } from 'react';
import { type Field, FieldType } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import type { RecipientColorStyles } from '../../lib/recipient-colors';
@ -23,6 +25,11 @@ export function FieldContainerPortal({
const alternativePortalRoot = document.getElementById('document-field-portal-root');
const coords = useFieldPageCoords(field);
const $pageBounds = useElementBounds(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
);
const maxWidth = $pageBounds?.width ? $pageBounds.width - coords.x : undefined;
const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO';
@ -32,10 +39,14 @@ export function FieldContainerPortal({
const bounds = {
top: `${coords.y}px`,
left: `${coords.x}px`,
...(!isCheckboxOrRadioField && {
height: `${coords.height}px`,
width: `${coords.width}px`,
}),
...(!isCheckboxOrRadioField
? {
height: `${coords.height}px`,
width: `${coords.width}px`,
}
: {
maxWidth: `${maxWidth}px`,
}),
};
if (portalBounds) {
@ -44,7 +55,7 @@ export function FieldContainerPortal({
}
return bounds;
}, [coords, isCheckboxOrRadioField]);
}, [coords, maxWidth, isCheckboxOrRadioField]);
return createPortal(
<div className={cn('absolute', className)} style={style}>

View File

@ -206,8 +206,8 @@ export const AddSubjectFormPartial = ({
<p className="mt-2">
<Trans>
We will generate signing links for you, which you can send to the
recipients through your method of choice.
We will generate signing links for you, which you can send to the recipients
through your method of choice.
</Trans>
</p>
</div>

View File

@ -38,13 +38,8 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
const { type, fieldMeta } = field;
// Only render checkbox if values exist, otherwise render the empty checkbox field content.
if (
field.type === FieldType.CHECKBOX &&
field.fieldMeta?.type === 'checkbox' &&
field.fieldMeta.values &&
field.fieldMeta.values.length > 0
) {
// Render checkbox layout for checkbox fields, even if no values exist yet
if (field.type === FieldType.CHECKBOX && field.fieldMeta?.type === 'checkbox') {
let checkedValues: string[] = [];
try {
@ -55,8 +50,32 @@ export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
console.error(err);
}
// If no values exist yet, show a placeholder checkbox
if (!field.fieldMeta.values || field.fieldMeta.values.length === 0) {
return (
<div
className={cn(
'flex gap-1 py-0.5',
field.fieldMeta.direction === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col gap-y-1',
)}
>
<div className="flex items-center">
<Checkbox className="h-3 w-3" disabled />
<Label className="text-foreground ml-1.5 text-xs font-normal opacity-50">
Checkbox option
</Label>
</div>
</div>
);
}
return (
<div className="flex flex-col gap-y-1 py-0.5">
<div
className={cn(
'flex gap-1 py-0.5',
field.fieldMeta.direction === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col gap-y-1',
)}
>
{field.fieldMeta.values.map((item, index) => (
<div key={index} className="flex items-center">
<Checkbox

View File

@ -129,6 +129,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
validationLength: 0,
required: false,
readOnly: false,
direction: 'vertical',
};
case FieldType.DROPDOWN:
return {

View File

@ -8,6 +8,7 @@ import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
import { useSearchParams } from 'react-router';
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
@ -81,7 +82,11 @@ export const FieldItem = ({
pageWidth: defaultWidth || 0,
});
const [settingsActive, setSettingsActive] = useState(false);
const $el = useRef(null);
const $el = useRef<HTMLDivElement>(null);
const $pageBounds = useElementBounds(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
);
const signerStyles = useRecipientColors(recipientIndex);
@ -233,9 +238,10 @@ export const FieldItem = ({
default={{
x: coords.pageX,
y: coords.pageY,
height: fixedSize ? '' : coords.pageHeight,
width: fixedSize ? '' : coords.pageWidth,
height: fixedSize ? 'auto' : coords.pageHeight,
width: fixedSize ? 'auto' : coords.pageWidth,
}}
maxWidth={fixedSize && $pageBounds?.width ? $pageBounds.width - coords.pageX : undefined}
bounds={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`}
onDragStart={() => onFieldActivate?.()}
onResizeStart={() => onFieldActivate?.()}

View File

@ -44,6 +44,9 @@ export const CheckboxFieldAdvancedSettings = ({
const [required, setRequired] = useState(fieldState.required ?? false);
const [validationLength, setValidationLength] = useState(fieldState.validationLength ?? 0);
const [validationRule, setValidationRule] = useState(fieldState.validationRule ?? '');
const [direction, setDirection] = useState<'vertical' | 'horizontal'>(
fieldState.direction ?? 'vertical',
);
const handleToggleChange = (field: keyof CheckboxFieldMeta, value: string | boolean) => {
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
@ -52,11 +55,14 @@ export const CheckboxFieldAdvancedSettings = ({
field === 'validationRule' ? String(value) : String(fieldState.validationRule);
const validationLength =
field === 'validationLength' ? Number(value) : Number(fieldState.validationLength);
const currentDirection =
field === 'direction' && String(value) === 'horizontal' ? 'horizontal' : 'vertical';
setReadOnly(readOnly);
setRequired(required);
setValidationRule(validationRule);
setValidationLength(validationLength);
setDirection(currentDirection);
const errors = validateCheckboxField(
values.map((item) => item.value),
@ -65,6 +71,7 @@ export const CheckboxFieldAdvancedSettings = ({
required,
validationRule,
validationLength,
direction: currentDirection,
type: 'checkbox',
},
);
@ -86,6 +93,7 @@ export const CheckboxFieldAdvancedSettings = ({
required,
validationRule,
validationLength,
direction: direction,
type: 'checkbox',
},
);
@ -137,6 +145,29 @@ export const CheckboxFieldAdvancedSettings = ({
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div className="mb-2">
<Label>
<Trans>Direction</Trans>
</Label>
<Select
value={fieldState.direction ?? 'vertical'}
onValueChange={(val) => handleToggleChange('direction', val)}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectValue placeholder={_(msg`Select direction`)} />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="vertical">
<Trans>Vertical</Trans>
</SelectItem>
<SelectItem value="horizontal">
<Trans>Horizontal</Trans>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-row items-center gap-x-4">
<div className="flex w-2/3 flex-col">
<Label>