mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
chore: merged main
This commit is contained in:
@@ -54,7 +54,6 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
|
||||
if (plan[interval] && plan[interval].isVisibleInApp) {
|
||||
prices.push({
|
||||
...plan[interval],
|
||||
memberCount: plan.memberCount,
|
||||
claim: plan.id,
|
||||
});
|
||||
}
|
||||
@@ -120,12 +119,7 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
|
||||
<Trans>Subscribe</Trans>
|
||||
</IndividualPersonalLayoutCheckoutButton>
|
||||
) : (
|
||||
<BillingDialog
|
||||
priceId={price.id}
|
||||
planName={price.product.name}
|
||||
memberCount={price.memberCount}
|
||||
claim={price.claim}
|
||||
/>
|
||||
<BillingDialog priceId={price.id} planName={price.product.name} claim={price.claim} />
|
||||
)}
|
||||
</CardContent>
|
||||
</MotionCard>
|
||||
@@ -136,16 +130,7 @@ export const BillingPlans = ({ plans }: BillingPlansProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const BillingDialog = ({
|
||||
priceId,
|
||||
planName,
|
||||
claim,
|
||||
}: {
|
||||
priceId: string;
|
||||
planName: string;
|
||||
memberCount: number;
|
||||
claim: string;
|
||||
}) => {
|
||||
const BillingDialog = ({ priceId, planName, claim }: { priceId: string; planName: string; claim: string }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { TTemplate } from '@documenso/lib/types/template';
|
||||
import { isRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
@@ -17,6 +18,7 @@ import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
import { getDirectTemplateErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
import { DirectTemplateConfigureForm, type TDirectTemplateConfigureFormSchema } from './direct-template-configure-form';
|
||||
import { type DirectTemplateLocalField, DirectTemplateSigningForm } from './direct-template-signing-form';
|
||||
@@ -120,9 +122,12 @@ export const DirectTemplatePageView = ({
|
||||
await navigate(`/sign/${token}/complete`);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDirectTemplateErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`We were unable to submit this document at this time. Please try again later.`),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { TDocument } from '@documenso/lib/types/document';
|
||||
import { ZDocumentAccessAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||
@@ -25,9 +26,9 @@ import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type DocumentEditFormProps = {
|
||||
className?: string;
|
||||
@@ -387,9 +388,12 @@ export const DocumentEditForm = ({ className, initialDocument, documentRootPath
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
const error = AppError.parseError(err);
|
||||
const errorMessage = getDistributeErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while sending the document.`),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||
@@ -20,9 +20,9 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { FileRejection } from 'react-dropzone';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getUploadErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type DocumentUploadButtonLegacyProps = {
|
||||
className?: string;
|
||||
@@ -130,30 +130,11 @@ export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadBu
|
||||
|
||||
console.error(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => msg`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => msg`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with(
|
||||
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
|
||||
() => msg`You have reached the limit of the number of files per envelope.`,
|
||||
)
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => msg`This file type isn't supported. Please upload a PDF or Word document.`)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => msg`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => msg`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
)
|
||||
.otherwise(() => msg`An error occurred while uploading your document.`);
|
||||
const errorMessage = getUploadErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(errorMessage),
|
||||
title: _(errorMessage.title),
|
||||
description: _(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
+103
-2
@@ -14,13 +14,22 @@ import {
|
||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
||||
import { CommandDialog } from '@documenso/ui/primitives/command';
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import type { FieldType } from '@prisma/client';
|
||||
import Konva from 'konva';
|
||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||
import type { Transformer } from 'konva/lib/shapes/Transformer';
|
||||
import { CopyPlusIcon, SquareStackIcon, TrashIcon, UserCircleIcon } from 'lucide-react';
|
||||
import { CopyPlusIcon, ShapesIcon, SquareStackIcon, TrashIcon, UserCircleIcon } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
|
||||
@@ -470,6 +479,22 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
|
||||
}
|
||||
};
|
||||
|
||||
const changeSelectedFieldsType = (type: FieldType) => {
|
||||
const fields = selectedKonvaFieldGroups
|
||||
.map((field) => editorFields.getFieldByFormId(field.id()))
|
||||
.filter((field) => field !== undefined);
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.type !== type) {
|
||||
editorFields.updateFieldByFormId(field.formId, {
|
||||
type,
|
||||
fieldMeta: structuredClone(FIELD_META_DEFAULT_VALUES[type]),
|
||||
id: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const duplicatedSelectedFields = () => {
|
||||
const fields = selectedKonvaFieldGroups
|
||||
.map((field) => editorFields.getFieldByFormId(field.id()))
|
||||
@@ -554,6 +579,7 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
|
||||
handleDuplicateSelectedFieldsOnAllPages={duplicatedSelectedFieldsOnAllPages}
|
||||
handleDeleteSelectedFields={deletedSelectedFields}
|
||||
handleChangeRecipient={changeSelectedFieldsRecipients}
|
||||
handleChangeFieldType={changeSelectedFieldsType}
|
||||
selectedFieldFormId={selectedKonvaFieldGroups.map((field) => field.id())}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -602,6 +628,7 @@ type FieldActionButtonsProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
handleDuplicateSelectedFieldsOnAllPages: () => void;
|
||||
handleDeleteSelectedFields: () => void;
|
||||
handleChangeRecipient: (recipientId: number) => void;
|
||||
handleChangeFieldType: (type: FieldType) => void;
|
||||
selectedFieldFormId: string[];
|
||||
};
|
||||
|
||||
@@ -610,15 +637,40 @@ const FieldActionButtons = ({
|
||||
handleDuplicateSelectedFieldsOnAllPages,
|
||||
handleDeleteSelectedFields,
|
||||
handleChangeRecipient,
|
||||
handleChangeFieldType,
|
||||
selectedFieldFormId,
|
||||
...props
|
||||
}: FieldActionButtonsProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [showRecipientSelector, setShowRecipientSelector] = useState(false);
|
||||
const [showFieldTypeSelector, setShowFieldTypeSelector] = useState(false);
|
||||
|
||||
const { editorFields, envelope } = useCurrentEnvelopeEditor();
|
||||
|
||||
/**
|
||||
* Decide the preselected field type in the command input.
|
||||
*
|
||||
* If all fields share the same type, use that as the default selection.
|
||||
* Otherwise show no preselection.
|
||||
*/
|
||||
const preselectedFieldType = useMemo(() => {
|
||||
if (selectedFieldFormId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = editorFields.localFields.filter((field) => selectedFieldFormId.includes(field.formId));
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstType = fields[0].type;
|
||||
const isTypesSame = fields.every((field) => field.type === firstType);
|
||||
|
||||
return isTypesSame ? firstType : null;
|
||||
}, [editorFields.localFields, selectedFieldFormId]);
|
||||
|
||||
/**
|
||||
* Decide the preselected recipient in the command input.
|
||||
*
|
||||
@@ -656,6 +708,7 @@ const FieldActionButtons = ({
|
||||
<div className="flex flex-col items-center" {...props}>
|
||||
<div className="group flex w-fit items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title={t`Change Recipient`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={() => setShowRecipientSelector(true)}
|
||||
@@ -665,6 +718,17 @@ const FieldActionButtons = ({
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title={t`Change Field Type`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={() => setShowFieldTypeSelector(true)}
|
||||
onTouchEnd={() => setShowFieldTypeSelector(true)}
|
||||
>
|
||||
<ShapesIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title={t`Duplicate`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={handleDuplicateSelectedFields}
|
||||
@@ -674,6 +738,7 @@ const FieldActionButtons = ({
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title={t`Duplicate on all pages`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={handleDuplicateSelectedFieldsOnAllPages}
|
||||
@@ -683,6 +748,7 @@ const FieldActionButtons = ({
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title={t`Remove`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={handleDeleteSelectedFields}
|
||||
@@ -705,6 +771,41 @@ const FieldActionButtons = ({
|
||||
fields={envelope.fields}
|
||||
/>
|
||||
</CommandDialog>
|
||||
|
||||
<CommandDialog position="start" open={showFieldTypeSelector} onOpenChange={setShowFieldTypeSelector}>
|
||||
<Command defaultValue={preselectedFieldType ? t(FRIENDLY_FIELD_TYPE[preselectedFieldType]) : undefined}>
|
||||
<CommandInput placeholder={t`Select a field type`} />
|
||||
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<span className="inline-block px-4 text-muted-foreground">
|
||||
{t`No field type matching this description was found.`}
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{fieldButtonList.map((field) => {
|
||||
const FieldIcon = field.icon;
|
||||
const label = t(FRIENDLY_FIELD_TYPE[field.type]);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={field.type}
|
||||
className="px-2"
|
||||
onSelect={() => {
|
||||
handleChangeFieldType(field.type);
|
||||
setShowFieldTypeSelector(false);
|
||||
}}
|
||||
>
|
||||
<FieldIcon className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{label}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</CommandDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@documenso/ui/components/recipient/recipient-autocomplete-input';
|
||||
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@documenso/ui/primitives/card';
|
||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||
@@ -586,6 +587,9 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
}
|
||||
}, [formValues]);
|
||||
|
||||
const recipientCountLimit = organisation.organisationClaim.recipientCount;
|
||||
const isOverRecipientLimit = recipientCountLimit > 0 && signers.length > recipientCountLimit;
|
||||
|
||||
return (
|
||||
<Card backdropBlur={false} className="border">
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
@@ -650,6 +654,17 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{isOverRecipientLimit && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
This envelope cannot have more than {recipientCountLimit} recipients. Please contact support if you need
|
||||
more.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<div
|
||||
className={cn('-mt-2 mb-2 space-y-4 rounded-md bg-accent/50 p-4', {
|
||||
|
||||
+42
-10
@@ -9,7 +9,7 @@ import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { DocumentEmailEvents, ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
type TDocumentMetaDateFormat,
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
@@ -39,6 +39,7 @@ import { ExpirationPeriodPicker } from '@documenso/ui/components/document/expira
|
||||
import { ReminderSettingsPicker } from '@documenso/ui/components/document/reminder-settings-picker';
|
||||
import { TemplateTypeSelect, TemplateTypeTooltip } from '@documenso/ui/components/template/template-type-select';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { CardDescription, CardHeader, CardTitle } from '@documenso/ui/primitives/card';
|
||||
import { Combobox } from '@documenso/ui/primitives/combobox';
|
||||
@@ -114,7 +115,7 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
type EnvelopeEditorSettingsTabType = 'general' | 'reminders' | 'email' | 'security';
|
||||
type EnvelopeEditorSettingsTabType = 'general' | 'reminders' | 'notifications' | 'security';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@@ -130,10 +131,10 @@ const tabs = [
|
||||
description: msg`Configure signing reminder settings for the document.`,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
title: msg`Email`,
|
||||
id: 'notifications',
|
||||
title: msg`Notifications`,
|
||||
icon: MailIcon,
|
||||
description: msg`Configure email settings for the document.`,
|
||||
description: msg`Configure notification settings for the document.`,
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
@@ -143,6 +144,18 @@ const tabs = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Recipient-facing notification events. These are suppressed at send time
|
||||
// when distributionMethod is not EMAIL (see extractDerivedDocumentEmailSettings),
|
||||
// so the UI mirrors that by disabling the matching checkboxes.
|
||||
const RECIPIENT_EMAIL_EVENTS = [
|
||||
DocumentEmailEvents.RecipientSigningRequest,
|
||||
DocumentEmailEvents.RecipientRemoved,
|
||||
DocumentEmailEvents.RecipientSigned,
|
||||
DocumentEmailEvents.DocumentPending,
|
||||
DocumentEmailEvents.DocumentCompleted,
|
||||
DocumentEmailEvents.DocumentDeleted,
|
||||
] as const;
|
||||
|
||||
type TAddSettingsFormSchema = z.infer<typeof ZAddSettingsFormSchema>;
|
||||
|
||||
type EnvelopeEditorSettingsDialogProps = {
|
||||
@@ -205,6 +218,8 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
);
|
||||
|
||||
const emailSettings = form.watch('meta.emailSettings');
|
||||
const distributionMethod = form.watch('meta.distributionMethod');
|
||||
const isEmailDistribution = distributionMethod === DocumentDistributionMethod.EMAIL;
|
||||
|
||||
const { data: emailData, isLoading: isLoadingEmails } = trpc.enterprise.organisation.email.find.useQuery(
|
||||
{
|
||||
@@ -334,7 +349,7 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
|
||||
<nav className="col-span-12 mb-8 flex flex-wrap items-center justify-start gap-x-2 gap-y-4 px-4 md:col-span-3 md:w-full md:flex-col md:items-start md:gap-y-2">
|
||||
{tabs.map((tab) => {
|
||||
if (tab.id === 'email' && !settings.allowConfigureDistribution) {
|
||||
if (tab.id === 'notifications' && !settings.allowConfigureDistribution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -730,7 +745,7 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
)}
|
||||
/>
|
||||
))
|
||||
.with({ activeTab: 'email', settings: { allowConfigureDistribution: true } }, () => (
|
||||
.with({ activeTab: 'notifications', settings: { allowConfigureDistribution: true } }, () => (
|
||||
<>
|
||||
{settings.allowConfigureEmailSender && organisation.organisationClaim.flags.emailDomains && (
|
||||
<FormField
|
||||
@@ -747,6 +762,7 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value}
|
||||
onValueChange={(value) => field.onChange(value === '-1' ? null : value)}
|
||||
disabled={!isEmailDistribution}
|
||||
>
|
||||
<SelectTrigger loading={isLoadingEmails} className="bg-background">
|
||||
<SelectValue />
|
||||
@@ -783,7 +799,7 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={!isEmailDistribution} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -804,7 +820,7 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={!isEmailDistribution} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -832,7 +848,11 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea className="h-16 resize-none bg-background" {...field} />
|
||||
<Textarea
|
||||
className="h-16 resize-none bg-background"
|
||||
{...field}
|
||||
disabled={!isEmailDistribution}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
@@ -843,7 +863,19 @@ export const EnvelopeEditorSettingsDialog = ({ trigger, ...props }: EnvelopeEdit
|
||||
<DocumentEmailCheckboxes
|
||||
value={emailSettings}
|
||||
onChange={(value) => form.setValue('meta.emailSettings', value)}
|
||||
hiddenEvents={isEmailDistribution ? undefined : RECIPIENT_EMAIL_EVENTS}
|
||||
/>
|
||||
|
||||
{!isEmailDistribution && (
|
||||
<Alert variant="warning">
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Email distribution needs to be enabled in the general settings tab to configure recipient
|
||||
email related settings.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
.with({ activeTab: 'security' }, () => (
|
||||
|
||||
+22
-9
@@ -9,6 +9,10 @@ import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { ZFullFieldSchema } from '@documenso/lib/types/field';
|
||||
import {
|
||||
createFieldCanvasStyleCache,
|
||||
type FieldCanvasStyleCache,
|
||||
} from '@documenso/lib/universal/field-renderer/field-canvas-style';
|
||||
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
|
||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
@@ -135,7 +139,10 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
});
|
||||
}, [envelope.recipients, pageNumber, currentEnvelopeItem?.id]);
|
||||
|
||||
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||
const unsafeRenderFieldOnLayer = (
|
||||
unparsedField: Field & { signature?: Signature | null },
|
||||
fieldCanvasStyleCache: FieldCanvasStyleCache,
|
||||
) => {
|
||||
if (!pageLayer.current) {
|
||||
console.error('Layer not loaded yet');
|
||||
return;
|
||||
@@ -143,11 +150,9 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
|
||||
const fieldToRender = ZFullFieldSchema.parse(unparsedField);
|
||||
|
||||
const color = fieldToRender.fieldMeta?.readOnly
|
||||
? 'readOnly'
|
||||
: showPendingFieldTooltip && isFieldUnsignedAndRequired(fieldToRender)
|
||||
? 'orange'
|
||||
: 'green';
|
||||
const isValidating = showPendingFieldTooltip && isFieldUnsignedAndRequired(fieldToRender);
|
||||
|
||||
const color = fieldToRender.fieldMeta?.readOnly ? 'readOnly' : isValidating ? 'orange' : 'green';
|
||||
|
||||
const { fieldGroup } = renderField({
|
||||
scale,
|
||||
@@ -159,6 +164,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
height: Number(fieldToRender.height),
|
||||
positionX: Number(fieldToRender.positionX),
|
||||
positionY: Number(fieldToRender.positionY),
|
||||
isValidating,
|
||||
signature: unparsedField.signature,
|
||||
},
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
@@ -166,6 +172,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
pageHeight: unscaledViewport.height,
|
||||
color,
|
||||
mode: 'sign',
|
||||
fieldCanvasStyleCache,
|
||||
});
|
||||
|
||||
const handleFieldGroupClick = (e: KonvaEventObject<Event>) => {
|
||||
@@ -411,9 +418,12 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
fieldGroup.on('pointerdown', handleFieldGroupClick);
|
||||
};
|
||||
|
||||
const renderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||
const renderFieldOnLayer = (
|
||||
unparsedField: Field & { signature?: Signature | null },
|
||||
fieldCanvasStyleCache: FieldCanvasStyleCache,
|
||||
) => {
|
||||
try {
|
||||
unsafeRenderFieldOnLayer(unparsedField);
|
||||
unsafeRenderFieldOnLayer(unparsedField, fieldCanvasStyleCache);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setRenderError(true);
|
||||
@@ -426,6 +436,8 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldCanvasStyleCache = createFieldCanvasStyleCache();
|
||||
|
||||
// Render current recipient fields which have changed or are not currently rendered.
|
||||
for (const field of localPageFields) {
|
||||
const existingCachedField = cachedRenderFields.current.get(field.id);
|
||||
@@ -437,7 +449,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
existingCachedField.inserted !== field.inserted ||
|
||||
existingCachedField.customText !== field.customText
|
||||
) {
|
||||
renderFieldOnLayer(field);
|
||||
renderFieldOnLayer(field, fieldCanvasStyleCache);
|
||||
cachedRenderFields.current.set(field.id, field);
|
||||
}
|
||||
}
|
||||
@@ -463,6 +475,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
|
||||
color: 'readOnly',
|
||||
editable: false,
|
||||
mode: 'sign',
|
||||
fieldCanvasStyleCache,
|
||||
});
|
||||
|
||||
// Other-recipient fields are display-only — they have no click handlers
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { getAllowedUploadMimeTypes } from '@documenso/lib/constants/document-conversion';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -20,9 +20,9 @@ import { Loader } from 'lucide-react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { ErrorCode as DropzoneErrorCode, type FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { Link, useNavigate, useParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getUploadErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export interface EnvelopeDropZoneWrapperProps {
|
||||
children: ReactNode;
|
||||
@@ -109,27 +109,11 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with('ENVELOPE_ITEM_LIMIT_EXCEEDED', () => t`You have reached the limit of the number of files per envelope.`)
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => t`This file type isn't supported. Please upload a PDF or Word document.`)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => t`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => t`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
)
|
||||
.otherwise(() => t`An error occurred during upload.`);
|
||||
const errorMessage = getUploadErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: errorMessage,
|
||||
title: i18n._(errorMessage.title),
|
||||
description: i18n._(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
@@ -17,9 +17,9 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ErrorCode as DropzoneErrorCode, type FileRejection } from 'react-dropzone';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
import { getUploadErrorMessage } from '~/utils/toast-error-messages';
|
||||
|
||||
export type EnvelopeUploadButtonProps = {
|
||||
className?: string;
|
||||
@@ -112,27 +112,11 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
||||
|
||||
console.error(err);
|
||||
|
||||
const errorMessage = match(error.code)
|
||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
||||
.with(
|
||||
AppErrorCode.LIMIT_EXCEEDED,
|
||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||
)
|
||||
.with('ENVELOPE_ITEM_LIMIT_EXCEEDED', () => t`You have reached the limit of the number of files per envelope.`)
|
||||
.with('UNSUPPORTED_FILE_TYPE', () => t`This file type isn't supported. Please upload a PDF or Word document.`)
|
||||
.with(
|
||||
'CONVERSION_SERVICE_UNAVAILABLE',
|
||||
() => t`Document conversion is temporarily unavailable. Please try again shortly or upload a PDF.`,
|
||||
)
|
||||
.with(
|
||||
'CONVERSION_FAILED',
|
||||
() => t`We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead.`,
|
||||
)
|
||||
.otherwise(() => t`An error occurred while uploading your document.`);
|
||||
const errorMessage = getUploadErrorMessage(error.code);
|
||||
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: errorMessage,
|
||||
title: i18n._(errorMessage.title),
|
||||
description: i18n._(errorMessage.description),
|
||||
variant: 'destructive',
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useOptionalCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { SUPPORT_EMAIL } from '@documenso/lib/constants/app';
|
||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META } from '@documenso/lib/constants/trpc';
|
||||
import { INTERNAL_CLAIM_ID } from '@documenso/lib/types/subscription';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
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 { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const OrganisationQuotaBanner = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const organisation = useOptionalCurrentOrganisation();
|
||||
|
||||
const { data: quotaFlags } = trpc.organisation.getQuotaFlags.useQuery(
|
||||
{ organisationId: organisation?.id ?? '' },
|
||||
{
|
||||
enabled: Boolean(organisation),
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
...SKIP_QUERY_BATCH_META,
|
||||
refetchInterval: 1000 * 60,
|
||||
refetchIntervalInBackground: false,
|
||||
},
|
||||
);
|
||||
|
||||
const isAnyQuotaExceeded = Boolean(
|
||||
quotaFlags?.isDocumentQuotaExceeded || quotaFlags?.isEmailQuotaExceeded || quotaFlags?.isApiQuotaExceeded,
|
||||
);
|
||||
|
||||
// Every member of the organisation sees the banner when a quota is exhausted.
|
||||
// Note: Skipping free plan banner for now because their quota can incorrectly show as exceeded.
|
||||
if (
|
||||
!organisation ||
|
||||
!quotaFlags ||
|
||||
!isAnyQuotaExceeded ||
|
||||
organisation.organisationClaim.originalSubscriptionClaimId === INTERNAL_CLAIM_ID.FREE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-yellow-200 text-yellow-900 dark:bg-yellow-400">
|
||||
<div className="mx-auto flex max-w-screen-xl items-center justify-center gap-x-4 px-4 py-2 font-medium text-sm">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="mr-2.5 h-5 w-5" />
|
||||
|
||||
<Trans>Your organisation has exceeded a fair use limit</Trans>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-500"
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="sm"
|
||||
>
|
||||
<Trans>Learn more</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Fair use limit exceeded</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Your organisation has exceeded a fair use limit. Please contact{' '}
|
||||
<a className="text-primary" href={`mailto:${SUPPORT_EMAIL}`}>
|
||||
support
|
||||
</a>{' '}
|
||||
to review your plan's limits.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="neutral">
|
||||
<AlertDescription>
|
||||
<ul className="list-inside list-disc text-sm">
|
||||
{quotaFlags.isDocumentQuotaExceeded && (
|
||||
<li className="list-disc">
|
||||
<Trans>Document creation has been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isEmailQuotaExceeded && (
|
||||
<li className="list-disc">
|
||||
<Trans>Email sending has been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
{quotaFlags.isApiQuotaExceeded && (
|
||||
<li className="list-disc">
|
||||
<Trans>API requests have been temporarily paused</Trans>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
<Trans>Close</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user