chore: merged main

This commit is contained in:
Catalin Pit
2025-07-23 14:03:30 +03:00
93 changed files with 12721 additions and 546 deletions

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

@ -10,6 +10,7 @@ import { useRevalidator } from 'react-router';
import { P, match } from 'ts-pattern';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { AUTO_SIGNABLE_FIELD_TYPES } from '@documenso/lib/constants/autosign';
import { DocumentAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { trpc } from '@documenso/trpc/react';
@ -30,13 +31,6 @@ import { DocumentSigningDisclosure } from '~/components/general/document-signing
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { useRequiredDocumentSigningContext } from './document-signing-provider';
const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.DATE,
];
// The action auth types that are not allowed to be auto signed
//
// Reasoning: If the action auth is a passkey or 2FA, it's likely that the owner of the document

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}`;
@ -286,6 +294,7 @@ export const DocumentSigningCheckboxField = ({
className="h-3 w-3"
id={`checkbox-${field.id}-${item.id}`}
checked={checkedValues.includes(itemValue)}
disabled={isReadOnly}
onCheckedChange={() => handleCheckboxChange(item.value, item.id)}
/>
{!item.value.includes('empty-value-') && item.value && (
@ -304,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}`;
@ -314,7 +328,7 @@ export const DocumentSigningCheckboxField = ({
className="h-3 w-3"
id={`checkbox-${field.id}-${item.id}`}
checked={parsedCheckedValues.includes(itemValue)}
disabled={isLoading}
disabled={isLoading || isReadOnly}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>
{!item.value.includes('empty-value-') && item.value && (

View File

@ -131,7 +131,12 @@ export const DocumentSigningFieldContainer = ({
return (
<div className={cn('[container-type:size]')}>
<FieldRootContainer color={RECIPIENT_COLOR_STYLES.green} field={field}>
<FieldRootContainer
color={
field.fieldMeta?.readOnly ? RECIPIENT_COLOR_STYLES.readOnly : RECIPIENT_COLOR_STYLES.green
}
field={field}
>
{!field.inserted && !loading && !readOnlyField && (
<button
type="submit"
@ -140,14 +145,6 @@ export const DocumentSigningFieldContainer = ({
/>
)}
{readOnlyField && (
<button className="bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100">
<span className="bg-foreground/50 text-background rounded-xl p-2">
<Trans>Read only field</Trans>
</span>
</button>
)}
{type === 'Checkbox' && field.inserted && !loading && !readOnlyField && (
<button
className="absolute -bottom-10 flex items-center justify-evenly rounded-md border bg-gray-900 opacity-0 group-hover:opacity-100"

View File

@ -34,7 +34,7 @@ export const DocumentSigningFieldsInserted = ({
textAlign = 'left',
}: DocumentSigningFieldsInsertedProps) => {
return (
<div className="flex h-full w-full items-center">
<div className="flex h-full w-full items-center overflow-hidden">
<p
className={cn(
'text-foreground w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',

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

@ -73,9 +73,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

@ -41,6 +41,7 @@ export const DocumentSigningRadioField = ({
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
const isReadOnly = parsedFieldMeta.readOnly;
const values = parsedFieldMeta.values?.map((item) => ({
...item,
value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
@ -164,6 +165,7 @@ export const DocumentSigningRadioField = ({
value={item.value}
id={`option-${field.id}-${item.id}`}
checked={item.checked}
disabled={isReadOnly}
/>
{!item.value.includes('empty-value-') && item.value && (
<Label
@ -187,6 +189,7 @@ export const DocumentSigningRadioField = ({
value={item.value}
id={`option-${field.id}-${item.id}`}
checked={item.value === field.customText}
disabled={isReadOnly}
/>
{!item.value.includes('empty-value-') && item.value && (
<Label

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

@ -262,9 +262,7 @@ export const DocumentSigningTextField = ({
{field.inserted && (
<DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
{field.customText.length < 20
? field.customText
: field.customText.substring(0, 20) + '...'}
{field.customText}
</DocumentSigningFieldsInserted>
)}

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"
}