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