mirror of
https://github.com/documenso/documenso.git
synced 2026-08-21 14:01:48 +10:00
feat: unify settings (#3128)
This commit is contained in:
@@ -29,6 +29,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { useCspNonce } from '~/utils/nonce';
|
||||
|
||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||
import { InheritableField } from './inheritable-field';
|
||||
|
||||
const ZBrandingPreferencesFormSchema = z.object({
|
||||
brandingEnabled: z.boolean().nullable(),
|
||||
@@ -210,11 +211,13 @@ export function BrandingPreferencesForm({
|
||||
control={form.control}
|
||||
name="brandingEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Enable Custom Branding</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Enable Custom Branding</Trans>}
|
||||
testId="branding-enabled"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
@@ -252,7 +255,7 @@ export function BrandingPreferencesForm({
|
||||
<Trans>Enable custom branding for all documents in this organisation</Trans>
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -263,11 +266,13 @@ export function BrandingPreferencesForm({
|
||||
control={form.control}
|
||||
name="brandingLogo"
|
||||
render={({ field: { value: _value, onChange, ...field } }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Branding Logo</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={!previewUrl}
|
||||
label={<Trans>Branding Logo</Trans>}
|
||||
testId="branding-logo"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="relative h-48 w-full overflow-hidden rounded-lg border border-border bg-background">
|
||||
{previewUrl ? (
|
||||
@@ -345,7 +350,7 @@ export function BrandingPreferencesForm({
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -353,11 +358,13 @@ export function BrandingPreferencesForm({
|
||||
control={form.control}
|
||||
name="brandingUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Brand Website</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={!field.value}
|
||||
label={<Trans>Brand Website</Trans>}
|
||||
testId="branding-url"
|
||||
>
|
||||
<FormControl>
|
||||
<Input type="url" placeholder="https://example.com" disabled={!isBrandingEnabled} {...field} />
|
||||
</FormControl>
|
||||
@@ -372,7 +379,7 @@ export function BrandingPreferencesForm({
|
||||
</span>
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -380,11 +387,13 @@ export function BrandingPreferencesForm({
|
||||
control={form.control}
|
||||
name="brandingCompanyDetails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Brand Details</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={!field.value}
|
||||
label={<Trans>Brand Details</Trans>}
|
||||
testId="branding-company-details"
|
||||
>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t`Enter your brand details`}
|
||||
@@ -404,7 +413,7 @@ export function BrandingPreferencesForm({
|
||||
</span>
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { Form, FormControl, FormDescription, FormField } from '@documenso/ui/primitives/form/form';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { TeamGlobalSettings } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||
import { InheritableField } from './inheritable-field';
|
||||
|
||||
const ZCertificatePreferencesFormSchema = z.object({
|
||||
includeSigningCertificate: z.boolean().nullable(),
|
||||
includeAuditLog: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
export type TCertificatePreferencesFormSchema = z.infer<typeof ZCertificatePreferencesFormSchema>;
|
||||
|
||||
type SettingsSubset = Pick<TeamGlobalSettings, 'includeSigningCertificate' | 'includeAuditLog'>;
|
||||
|
||||
export type CertificatePreferencesFormProps = {
|
||||
settings: SettingsSubset;
|
||||
canInherit: boolean;
|
||||
onFormSubmit: (data: TCertificatePreferencesFormSchema) => Promise<void>;
|
||||
};
|
||||
|
||||
export const CertificatePreferencesForm = ({ settings, canInherit, onFormSubmit }: CertificatePreferencesFormProps) => {
|
||||
const form = useForm<TCertificatePreferencesFormSchema>({
|
||||
defaultValues: {
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
},
|
||||
resolver: zodResolver(ZCertificatePreferencesFormSchema),
|
||||
});
|
||||
|
||||
const handleFormSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
await onFormSubmit(data);
|
||||
} catch {
|
||||
// The page handler surfaces its own error toast. Keep the form dirty so
|
||||
// the save bar stays visible and the user can retry.
|
||||
return;
|
||||
}
|
||||
|
||||
form.reset(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeSigningCertificate"
|
||||
render={({ field }) => (
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Include the Signing Certificate in the Document</Trans>}
|
||||
testId="include-signing-certificate"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="bg-background text-muted-foreground"
|
||||
data-testid="include-signing-certificate-trigger"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls whether the signing certificate will be included in the document when it is downloaded. The
|
||||
signing certificate can still be downloaded from the logs page separately.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeAuditLog"
|
||||
render={({ field }) => (
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Include the Audit Logs in the Document</Trans>}
|
||||
testId="include-audit-log"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="bg-background text-muted-foreground"
|
||||
data-testid="include-audit-log-trigger"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls whether the audit logs will be included in the document when it is downloaded. The audit
|
||||
logs can still be downloaded from the logs page separately.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormStickySaveBar
|
||||
isDirty={form.formState.isDirty}
|
||||
isSubmitting={form.formState.isSubmitting}
|
||||
onReset={() => form.reset()}
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -2,11 +2,6 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { DATE_FORMATS } from '@documenso/lib/constants/date-formats';
|
||||
import { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||
import {
|
||||
type TEnvelopeExpirationPeriod,
|
||||
ZEnvelopeExpirationPeriod,
|
||||
} from '@documenso/lib/constants/envelope-expiration';
|
||||
import { type TEnvelopeReminderSettings, ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-reminder';
|
||||
import { isValidLanguageCode, SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import type { TDefaultRecipients } from '@documenso/lib/types/default-recipients';
|
||||
@@ -16,28 +11,17 @@ import { generateDefaultOrganisationSettings, isPersonalLayout } from '@documens
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { extractTeamSignatureSettings, generateDefaultTeamSettings } from '@documenso/lib/utils/teams';
|
||||
import { DocumentSignatureSettingsTooltip } from '@documenso/ui/components/document/document-signature-settings-tooltip';
|
||||
import { ExpirationPeriodPicker } from '@documenso/ui/components/document/expiration-period-picker';
|
||||
import { ReminderSettingsPicker } from '@documenso/ui/components/document/reminder-settings-picker';
|
||||
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
|
||||
import { Alert } from '@documenso/ui/primitives/alert';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Combobox } from '@documenso/ui/primitives/combobox';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Form, FormControl, FormDescription, FormField, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentVisibility, OrganisationType, type RecipientRole, type TeamGlobalSettings } from '@prisma/client';
|
||||
import { DocumentVisibility, type RecipientRole, type TeamGlobalSettings } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -46,6 +30,7 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox';
|
||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||
import { InheritableField } from './inheritable-field';
|
||||
|
||||
/**
|
||||
* Can't infer this from the schema since we need to keep the schema inside the component to allow
|
||||
@@ -56,15 +41,10 @@ export type TDocumentPreferencesFormSchema = {
|
||||
documentLanguage: (typeof SUPPORTED_LANGUAGE_CODES)[number] | null;
|
||||
documentTimezone: string | null;
|
||||
documentDateFormat: TDocumentMetaDateFormat | null;
|
||||
includeSenderDetails: boolean | null;
|
||||
includeSigningCertificate: boolean | null;
|
||||
includeAuditLog: boolean | null;
|
||||
signatureTypes: DocumentSignatureType[];
|
||||
defaultRecipients: TDefaultRecipients | null;
|
||||
delegateDocumentOwnership: boolean | null;
|
||||
aiFeaturesEnabled: boolean | null;
|
||||
envelopeExpirationPeriod: TEnvelopeExpirationPeriod | null;
|
||||
reminderSettings: TEnvelopeReminderSettings | null;
|
||||
};
|
||||
|
||||
type SettingsSubset = Pick<
|
||||
@@ -73,17 +53,12 @@ type SettingsSubset = Pick<
|
||||
| 'documentLanguage'
|
||||
| 'documentTimezone'
|
||||
| 'documentDateFormat'
|
||||
| 'includeSenderDetails'
|
||||
| 'includeSigningCertificate'
|
||||
| 'includeAuditLog'
|
||||
| 'typedSignatureEnabled'
|
||||
| 'uploadSignatureEnabled'
|
||||
| 'drawSignatureEnabled'
|
||||
| 'defaultRecipients'
|
||||
| 'delegateDocumentOwnership'
|
||||
| 'aiFeaturesEnabled'
|
||||
| 'envelopeExpirationPeriod'
|
||||
| 'reminderSettings'
|
||||
>;
|
||||
|
||||
export type DocumentPreferencesFormProps = {
|
||||
@@ -101,15 +76,10 @@ const getDocumentPreferencesFormValues = (settings: SettingsSubset): TDocumentPr
|
||||
documentLanguage: isValidLanguageCode(settings.documentLanguage) ? settings.documentLanguage : null,
|
||||
documentTimezone: settings.documentTimezone,
|
||||
documentDateFormat: parsedDocumentDateFormat.success ? parsedDocumentDateFormat.data : null,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
includeSigningCertificate: settings.includeSigningCertificate,
|
||||
includeAuditLog: settings.includeAuditLog,
|
||||
signatureTypes: extractTeamSignatureSettings({ ...settings }),
|
||||
defaultRecipients: settings.defaultRecipients ? ZDefaultRecipientsSchema.parse(settings.defaultRecipients) : null,
|
||||
delegateDocumentOwnership: settings.delegateDocumentOwnership,
|
||||
aiFeaturesEnabled: settings.aiFeaturesEnabled,
|
||||
envelopeExpirationPeriod: settings.envelopeExpirationPeriod ?? null,
|
||||
reminderSettings: settings.reminderSettings ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -120,31 +90,23 @@ export const DocumentPreferencesForm = ({
|
||||
isAiFeaturesConfigured = false,
|
||||
}: DocumentPreferencesFormProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { user, organisations } = useSession();
|
||||
const { organisations } = useSession();
|
||||
const currentOrganisation = useCurrentOrganisation();
|
||||
const optionalTeam = useOptionalCurrentTeam();
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
const isPersonalOrganisation = currentOrganisation.type === OrganisationType.PERSONAL;
|
||||
|
||||
const placeholderEmail = user.email ?? 'user@example.com';
|
||||
|
||||
const ZDocumentPreferencesFormSchema = z.object({
|
||||
documentVisibility: z.nativeEnum(DocumentVisibility).nullable(),
|
||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).nullable(),
|
||||
documentTimezone: z.string().nullable(),
|
||||
documentDateFormat: ZDocumentMetaDateFormatSchema.nullable(),
|
||||
includeSenderDetails: z.boolean().nullable(),
|
||||
includeSigningCertificate: z.boolean().nullable(),
|
||||
includeAuditLog: z.boolean().nullable(),
|
||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(canInherit ? 0 : 1, {
|
||||
message: msg`At least one signature type must be enabled`.id,
|
||||
}),
|
||||
defaultRecipients: ZDefaultRecipientsSchema.nullable(),
|
||||
delegateDocumentOwnership: z.boolean().nullable(),
|
||||
aiFeaturesEnabled: z.boolean().nullable(),
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullable(),
|
||||
reminderSettings: ZEnvelopeReminderSettings.nullable(),
|
||||
});
|
||||
|
||||
const defaultValues = getDocumentPreferencesFormValues(settings);
|
||||
@@ -189,17 +151,19 @@ export const DocumentPreferencesForm = ({
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<fieldset className="flex h-full max-w-2xl flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
{!isPersonalLayoutMode && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="documentVisibility"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Default Document Visibility</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Document Visibility</Trans>}
|
||||
testId="document-visibility"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
@@ -236,7 +200,7 @@ export const DocumentPreferencesForm = ({
|
||||
<FormDescription>
|
||||
<Trans>Controls the default visibility of an uploaded document.</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -245,11 +209,13 @@ export const DocumentPreferencesForm = ({
|
||||
control={form.control}
|
||||
name="documentLanguage"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Default Document Language</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Document Language</Trans>}
|
||||
testId="document-language"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
@@ -283,7 +249,7 @@ export const DocumentPreferencesForm = ({
|
||||
communications with the recipients.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -291,11 +257,12 @@ export const DocumentPreferencesForm = ({
|
||||
control={form.control}
|
||||
name="documentDateFormat"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Default Date Format</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Date Format</Trans>}
|
||||
testId="document-date-format"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value === null ? '-1' : field.value}
|
||||
@@ -322,7 +289,7 @@ export const DocumentPreferencesForm = ({
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -330,11 +297,12 @@ export const DocumentPreferencesForm = ({
|
||||
control={form.control}
|
||||
name="documentTimezone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Default Time Zone</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Time Zone</Trans>}
|
||||
testId="document-timezone"
|
||||
>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
triggerPlaceholder={canInherit ? t`Inherit from organisation` : t`Local timezone`}
|
||||
@@ -347,7 +315,7 @@ export const DocumentPreferencesForm = ({
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -355,12 +323,18 @@ export const DocumentPreferencesForm = ({
|
||||
control={form.control}
|
||||
name="signatureTypes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Default Signature Settings</Trans>
|
||||
<DocumentSignatureSettingsTooltip />
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={canInherit && (field.value === null || field.value.length === 0)}
|
||||
label={
|
||||
<span className="flex flex-row items-center">
|
||||
<Trans>Default Signature Settings</Trans>
|
||||
<DocumentSignatureSettingsTooltip />
|
||||
</span>
|
||||
}
|
||||
testId="signature-types"
|
||||
>
|
||||
<FormControl>
|
||||
<MultiSelectCombobox
|
||||
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
|
||||
@@ -383,179 +357,7 @@ export const DocumentPreferencesForm = ({
|
||||
<Trans>Controls which signatures are allowed to be used when signing a document.</Trans>
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isPersonalLayoutMode && !isPersonalOrganisation && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeSenderDetails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Send on Behalf of Team</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="bg-background text-muted-foreground"
|
||||
data-testid="include-sender-details-trigger"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<div className="pt-2">
|
||||
<div className="font-medium text-muted-foreground text-xs">
|
||||
<Trans>Preview</Trans>
|
||||
</div>
|
||||
|
||||
<Alert variant="neutral" className="mt-1 px-2.5 py-1.5 text-sm">
|
||||
{field.value ? (
|
||||
<Trans>
|
||||
"{placeholderEmail}" on behalf of "Team Name" has invited you to sign "example document".
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>"Team Name" has invited you to sign "example document".</Trans>
|
||||
)}
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls the formatting of the message that will be sent when inviting a recipient to sign a
|
||||
document. If a custom message has been provided while configuring the document, it will be used
|
||||
instead.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeSigningCertificate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Include the Signing Certificate in the Document</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="bg-background text-muted-foreground"
|
||||
data-testid="include-signing-certificate-trigger"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls whether the signing certificate will be included in the document when it is downloaded. The
|
||||
signing certificate can still be downloaded from the logs page separately.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeAuditLog"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Include the Audit Logs in the Document</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls whether the audit logs will be included in the document when it is downloaded. The audit
|
||||
logs can still be downloaded from the logs page separately.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -566,11 +368,13 @@ export const DocumentPreferencesForm = ({
|
||||
const recipients = field.value ?? [];
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Default Recipients</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Recipients</Trans>}
|
||||
testId="default-recipients"
|
||||
>
|
||||
{canInherit && (
|
||||
<Select
|
||||
value={field.value === null ? '-1' : '0'}
|
||||
@@ -638,7 +442,7 @@ export const DocumentPreferencesForm = ({
|
||||
<FormDescription>
|
||||
<Trans>Recipients that will be automatically added to new documents.</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -647,11 +451,13 @@ export const DocumentPreferencesForm = ({
|
||||
control={form.control}
|
||||
name="delegateDocumentOwnership"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Delegate Document Ownership</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Delegate Document Ownership</Trans>}
|
||||
testId="delegate-document-ownership"
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
@@ -681,65 +487,7 @@ export const DocumentPreferencesForm = ({
|
||||
<FormDescription>
|
||||
<Trans>Enable team API tokens to delegate document ownership to another team member.</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="envelopeExpirationPeriod"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Default Envelope Expiration</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<ExpirationPeriodPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
inheritLabel={canInherit ? t`Inherit from organisation` : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls how long recipients have to complete signing before the document expires. After expiration,
|
||||
recipients can no longer sign the document.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="reminderSettings"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Default Signing Reminders</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<ReminderSettingsPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
inheritLabel={canInherit ? t`Inherit from organisation` : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls when and how often reminder emails are sent to recipients who have not yet completed
|
||||
signing.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -748,11 +496,13 @@ export const DocumentPreferencesForm = ({
|
||||
control={form.control}
|
||||
name="aiFeaturesEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>AI Features</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>AI Features</Trans>}
|
||||
testId="ai-features-enabled"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
@@ -790,7 +540,7 @@ export const DocumentPreferencesForm = ({
|
||||
prefer European regions where available.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -806,7 +556,6 @@ export const DocumentPreferencesForm = ({
|
||||
onReset={handleResetToDefaults}
|
||||
showAiFeatures={isAiFeaturesConfigured}
|
||||
showDocumentVisibility={!isPersonalLayoutMode}
|
||||
showIncludeSenderDetails={!isPersonalLayoutMode && !isPersonalOrganisation}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { FROM_ADDRESS } from '@documenso/lib/constants/email';
|
||||
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS, ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import { zEmail } from '@documenso/lib/utils/zod';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { DocumentEmailCheckboxes } from '@documenso/ui/components/document/document-email-checkboxes';
|
||||
import { Alert } from '@documenso/ui/primitives/alert';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -17,22 +19,27 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { TeamGlobalSettings } from '@prisma/client';
|
||||
import { OrganisationType, type TeamGlobalSettings } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||
import { InheritableField } from './inheritable-field';
|
||||
|
||||
const ZEmailPreferencesFormSchema = z.object({
|
||||
emailId: z.string().nullable(),
|
||||
emailReplyTo: zEmail().nullable(),
|
||||
// emailReplyToName: z.string(),
|
||||
emailDocumentSettings: ZDocumentEmailSettingsSchema.nullable(),
|
||||
includeSenderDetails: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
export type TEmailPreferencesFormSchema = z.infer<typeof ZEmailPreferencesFormSchema>;
|
||||
|
||||
type SettingsSubset = Pick<TeamGlobalSettings, 'emailId' | 'emailReplyTo' | 'emailDocumentSettings'>;
|
||||
type SettingsSubset = Pick<
|
||||
TeamGlobalSettings,
|
||||
'emailId' | 'emailReplyTo' | 'emailDocumentSettings' | 'includeSenderDetails'
|
||||
>;
|
||||
|
||||
export type EmailPreferencesFormProps = {
|
||||
settings: SettingsSubset;
|
||||
@@ -41,14 +48,20 @@ export type EmailPreferencesFormProps = {
|
||||
};
|
||||
|
||||
export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: EmailPreferencesFormProps) => {
|
||||
const { user } = useSession();
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const isPersonalOrganisation = organisation.type === OrganisationType.PERSONAL;
|
||||
|
||||
const placeholderEmail = user.email ?? 'user@example.com';
|
||||
|
||||
const form = useForm<TEmailPreferencesFormSchema>({
|
||||
defaultValues: {
|
||||
emailId: settings.emailId,
|
||||
emailReplyTo: settings.emailReplyTo,
|
||||
// emailReplyToName: settings.emailReplyToName,
|
||||
emailDocumentSettings: settings.emailDocumentSettings,
|
||||
includeSenderDetails: settings.includeSenderDetails,
|
||||
},
|
||||
resolver: zodResolver(ZEmailPreferencesFormSchema),
|
||||
});
|
||||
@@ -75,7 +88,7 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<fieldset className="flex h-full max-w-2xl flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
{organisation.organisationClaim.flags.emailDomains && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -122,10 +135,12 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema
|
||||
control={form.control}
|
||||
name="emailReplyTo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Reply to email</Trans>
|
||||
</FormLabel>
|
||||
<InheritableField
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Reply to email</Trans>}
|
||||
testId="email-reply-to"
|
||||
>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
@@ -146,7 +161,7 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema
|
||||
</span>
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -170,10 +185,13 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema
|
||||
control={form.control}
|
||||
name="emailDocumentSettings"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Default Email Settings</Trans>
|
||||
</FormLabel>
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Email Settings</Trans>}
|
||||
testId="email-document-settings"
|
||||
>
|
||||
{canInherit && (
|
||||
<Select
|
||||
value={field.value === null ? 'INHERIT' : 'CONTROLLED'}
|
||||
@@ -212,10 +230,83 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema
|
||||
settings will not affect existing documents or templates.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!isPersonalOrganisation && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeSenderDetails"
|
||||
render={({ field }) => (
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Send on Behalf of Team</Trans>}
|
||||
testId="include-sender-details"
|
||||
>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value === null ? '-1' : field.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value === 'true' ? true : value === 'false' ? false : null)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="bg-background text-muted-foreground"
|
||||
data-testid="include-sender-details-trigger"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="true">
|
||||
<Trans>Yes</Trans>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value="false">
|
||||
<Trans>No</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{canInherit && (
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>Inherit from organisation</Trans>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<div className="pt-2">
|
||||
<div className="font-medium text-muted-foreground text-xs">
|
||||
<Trans>Preview</Trans>
|
||||
</div>
|
||||
|
||||
<Alert variant="neutral" className="mt-1 px-2.5 py-1.5 text-sm">
|
||||
{field.value ? (
|
||||
<Trans>
|
||||
"{placeholderEmail}" on behalf of "Team Name" has invited you to sign "example document".
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>"Team Name" has invited you to sign "example document".</Trans>
|
||||
)}
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls the formatting of the message that will be sent when inviting a recipient to sign a
|
||||
document. If a custom message has been provided while configuring the document, it will be used
|
||||
instead.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormStickySaveBar
|
||||
isDirty={form.formState.isDirty}
|
||||
isSubmitting={form.formState.isSubmitting}
|
||||
|
||||
@@ -5,6 +5,22 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { AlertTriangleIcon } from 'lucide-react';
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const getScrollParent = (node: HTMLElement): HTMLElement | null => {
|
||||
let current = node.parentElement;
|
||||
|
||||
while (current) {
|
||||
const { overflowY } = getComputedStyle(current);
|
||||
|
||||
if (overflowY === 'auto' || overflowY === 'scroll') {
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type FormStickySaveBarProps = {
|
||||
isDirty: boolean;
|
||||
isSubmitting: boolean;
|
||||
@@ -43,14 +59,18 @@ export const FormStickySaveBar = ({ isDirty, isSubmitting, onReset, resetToDefau
|
||||
}
|
||||
|
||||
// The sentinel sits at the bar's resting position (the end of the form). While the
|
||||
// bar is stuck to the bottom of the viewport the sentinel is scrolled past (out of
|
||||
// view); once you reach the form's end it comes into view and the bar settles.
|
||||
// bar is stuck to the bottom of the scroll container the sentinel is scrolled past
|
||||
// (out of view); once you reach the form's end it comes into view and the bar settles.
|
||||
//
|
||||
// Observe relative to the actual scroll container (not always the viewport) so a
|
||||
// banner shifting the page can't desync the detection from the sticky bar — both
|
||||
// then share the same reference box.
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsStuck(!entry.isIntersecting);
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
root: getScrollParent(sentinel),
|
||||
rootMargin: '0px 0px -24px 0px',
|
||||
threshold: 0,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type InheritableFieldProps = {
|
||||
isInherited: boolean;
|
||||
canInherit: boolean;
|
||||
label: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export const InheritableField = ({
|
||||
isInherited,
|
||||
canInherit,
|
||||
label,
|
||||
children,
|
||||
className,
|
||||
testId,
|
||||
}: InheritableFieldProps) => {
|
||||
if (!canInherit) {
|
||||
return (
|
||||
<FormItem className={className}>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
{children}
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem className={className} data-testid={testId ? `inheritable-${testId}` : undefined}>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
{label}
|
||||
<span
|
||||
className={cn(
|
||||
'rounded px-1.5 py-0.5 font-bold text-[9px] uppercase tracking-wide',
|
||||
isInherited
|
||||
? 'bg-muted text-muted-foreground'
|
||||
: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
|
||||
)}
|
||||
data-testid={testId ? `${testId}-status` : undefined}
|
||||
>
|
||||
{isInherited ? <Trans>Inherited</Trans> : <Trans>Override</Trans>}
|
||||
</span>
|
||||
</FormLabel>
|
||||
{children}
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
type TEnvelopeExpirationPeriod,
|
||||
ZEnvelopeExpirationPeriod,
|
||||
} from '@documenso/lib/constants/envelope-expiration';
|
||||
import { type TEnvelopeReminderSettings, ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-reminder';
|
||||
import { ExpirationPeriodPicker } from '@documenso/ui/components/document/expiration-period-picker';
|
||||
import { ReminderSettingsPicker } from '@documenso/ui/components/document/reminder-settings-picker';
|
||||
import { Form, FormControl, FormDescription, FormField, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type { TeamGlobalSettings } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FormStickySaveBar } from './form-sticky-save-bar';
|
||||
import { InheritableField } from './inheritable-field';
|
||||
|
||||
const ZReminderPreferencesFormSchema = z.object({
|
||||
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullable(),
|
||||
reminderSettings: ZEnvelopeReminderSettings.nullable(),
|
||||
});
|
||||
|
||||
export type TReminderPreferencesFormSchema = {
|
||||
envelopeExpirationPeriod: TEnvelopeExpirationPeriod | null;
|
||||
reminderSettings: TEnvelopeReminderSettings | null;
|
||||
};
|
||||
|
||||
type SettingsSubset = Pick<TeamGlobalSettings, 'envelopeExpirationPeriod' | 'reminderSettings'>;
|
||||
|
||||
export type ReminderPreferencesFormProps = {
|
||||
settings: SettingsSubset;
|
||||
canInherit: boolean;
|
||||
onFormSubmit: (data: TReminderPreferencesFormSchema) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ReminderPreferencesForm = ({ settings, canInherit, onFormSubmit }: ReminderPreferencesFormProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const form = useForm<TReminderPreferencesFormSchema>({
|
||||
defaultValues: {
|
||||
envelopeExpirationPeriod: settings.envelopeExpirationPeriod ?? null,
|
||||
reminderSettings: settings.reminderSettings ?? null,
|
||||
},
|
||||
resolver: zodResolver(ZReminderPreferencesFormSchema),
|
||||
});
|
||||
|
||||
const handleFormSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
await onFormSubmit(data);
|
||||
} catch {
|
||||
// The page handler surfaces its own error toast. Keep the form dirty so
|
||||
// the save bar stays visible and the user can retry.
|
||||
return;
|
||||
}
|
||||
|
||||
form.reset(data);
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<fieldset className="flex h-full flex-col gap-y-6" disabled={form.formState.isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="envelopeExpirationPeriod"
|
||||
render={({ field }) => (
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Envelope Expiration</Trans>}
|
||||
testId="envelope-expiration-period"
|
||||
>
|
||||
<FormControl>
|
||||
<ExpirationPeriodPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
inheritLabel={canInherit ? t`Inherit from organisation` : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls how long recipients have to complete signing before the document expires. After expiration,
|
||||
recipients can no longer sign the document.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="reminderSettings"
|
||||
render={({ field }) => (
|
||||
<InheritableField
|
||||
className="flex-1"
|
||||
canInherit={canInherit}
|
||||
isInherited={field.value === null}
|
||||
label={<Trans>Default Signing Reminders</Trans>}
|
||||
testId="reminder-settings"
|
||||
>
|
||||
<FormControl>
|
||||
<ReminderSettingsPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
inheritLabel={canInherit ? t`Inherit from organisation` : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls when and how often reminder emails are sent to recipients who have not yet completed
|
||||
signing.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
</InheritableField>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormStickySaveBar
|
||||
isDirty={form.formState.isDirty}
|
||||
isSubmitting={form.formState.isSubmitting}
|
||||
onReset={() => form.reset()}
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user