mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
feat: password reauthentication for documents and recipients (#1827)
Adds password reauthentication to our existing reauth providers, additionally swaps from an exclusive provider to an inclusive type where multiple methods can be selected to offer a this or that experience.
This commit is contained in:
@ -1,51 +1,75 @@
|
||||
import { forwardRef } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export const DocumentGlobalAuthAccessSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(props, ref) => {
|
||||
const { _ } = useLingui();
|
||||
export interface DocumentGlobalAuthAccessSelectProps {
|
||||
value?: string[];
|
||||
defaultValue?: string[];
|
||||
onValueChange?: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger ref={ref} className="bg-background text-muted-foreground">
|
||||
<SelectValue
|
||||
data-testid="documentAccessSelectValue"
|
||||
placeholder={_(msg`No restrictions`)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
export const DocumentGlobalAuthAccessSelect = ({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: DocumentGlobalAuthAccessSelectProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>No restrictions</Trans>
|
||||
</SelectItem>
|
||||
// Convert auth types to MultiSelect options
|
||||
const authOptions: Option[] = [
|
||||
{
|
||||
value: '-1',
|
||||
label: _(msg`No restrictions`),
|
||||
},
|
||||
...Object.values(DocumentAccessAuth).map((authType) => ({
|
||||
value: authType,
|
||||
label: DOCUMENT_AUTH_TYPES[authType].value,
|
||||
})),
|
||||
];
|
||||
|
||||
{Object.values(DocumentAccessAuth).map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
);
|
||||
// Convert string array to Option array for MultiSelect
|
||||
const selectedOptions =
|
||||
(value
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
|
||||
// Convert default value to Option array
|
||||
const defaultOptions =
|
||||
(defaultValue
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
|
||||
const handleChange = (options: Option[]) => {
|
||||
const values = options.map((option) => option.value);
|
||||
onValueChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
value={selectedOptions}
|
||||
defaultOptions={defaultOptions}
|
||||
options={authOptions}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder || _(msg`Select access methods`)}
|
||||
className="bg-background text-muted-foreground"
|
||||
hideClearAllButton={false}
|
||||
data-testid="documentAccessSelectValue"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DocumentGlobalAuthAccessSelect.displayName = 'DocumentGlobalAuthAccessSelect';
|
||||
|
||||
@ -63,7 +87,11 @@ export const DocumentGlobalAuthAccessTooltip = () => (
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>The authentication required for recipients to view the document.</Trans>
|
||||
<Trans>The authentication methods required for recipients to view the document.</Trans>
|
||||
</p>
|
||||
|
||||
<p className="mt-2">
|
||||
<Trans>Multiple access methods can be selected.</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||
|
||||
@ -1,54 +1,75 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export const DocumentGlobalAuthActionSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(props, ref) => {
|
||||
const { _ } = useLingui();
|
||||
export interface DocumentGlobalAuthActionSelectProps {
|
||||
value?: string[];
|
||||
defaultValue?: string[];
|
||||
onValueChange?: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue
|
||||
ref={ref}
|
||||
data-testid="documentActionSelectValue"
|
||||
placeholder={_(msg`No restrictions`)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
export const DocumentGlobalAuthActionSelect = ({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: DocumentGlobalAuthActionSelectProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>No restrictions</Trans>
|
||||
</SelectItem>
|
||||
// Convert auth types to MultiSelect options
|
||||
const authOptions: Option[] = [
|
||||
{
|
||||
value: '-1',
|
||||
label: _(msg`No restrictions`),
|
||||
},
|
||||
...Object.values(DocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||
.map((authType) => ({
|
||||
value: authType,
|
||||
label: DOCUMENT_AUTH_TYPES[authType].value,
|
||||
})),
|
||||
];
|
||||
|
||||
{Object.values(DocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||
.map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
);
|
||||
// Convert string array to Option array for MultiSelect
|
||||
const selectedOptions =
|
||||
(value
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
|
||||
// Convert default value to Option array
|
||||
const defaultOptions =
|
||||
(defaultValue
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
|
||||
const handleChange = (options: Option[]) => {
|
||||
const values = options.map((option) => option.value);
|
||||
onValueChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
value={selectedOptions}
|
||||
defaultOptions={defaultOptions}
|
||||
options={authOptions}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder || _(msg`Select authentication methods`)}
|
||||
className="bg-background text-muted-foreground"
|
||||
hideClearAllButton={false}
|
||||
data-testid="documentActionSelectValue"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DocumentGlobalAuthActionSelect.displayName = 'DocumentGlobalAuthActionSelect';
|
||||
|
||||
@ -64,20 +85,19 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>The authentication required for recipients to sign the signature field.</Trans>
|
||||
<Trans>
|
||||
The authentication methods required for recipients to sign the signature field.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This can be overriden by setting the authentication requirements directly on each
|
||||
recipient in the next step.
|
||||
These can be overriden by setting the authentication requirements directly on each
|
||||
recipient in the next step. Multiple methods can be selected.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||
{/* <li>
|
||||
<strong>Require account</strong> - The recipient must be signed in
|
||||
</li> */}
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||
@ -90,6 +110,14 @@ export const DocumentGlobalAuthActionTooltip = () => (
|
||||
their settings
|
||||
</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require password</strong> - The recipient must have an account and password
|
||||
configured via their settings, the password will be verified during signing
|
||||
</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>No restrictions</strong> - No authentication required
|
||||
|
||||
@ -3,97 +3,124 @@ import React from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { RecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export type RecipientActionAuthSelectProps = SelectProps;
|
||||
export interface RecipientActionAuthSelectProps {
|
||||
value?: string[];
|
||||
defaultValue?: string[];
|
||||
onValueChange?: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const RecipientActionAuthSelect = (props: RecipientActionAuthSelectProps) => {
|
||||
export const RecipientActionAuthSelect = ({
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: RecipientActionAuthSelectProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
// Convert auth types to MultiSelect options
|
||||
const authOptions: Option[] = [
|
||||
{
|
||||
value: '-1',
|
||||
label: _(msg`Inherit authentication method`),
|
||||
},
|
||||
...Object.values(RecipientActionAuth)
|
||||
.filter((auth) => auth !== RecipientActionAuth.ACCOUNT)
|
||||
.map((authType) => ({
|
||||
value: authType,
|
||||
label: DOCUMENT_AUTH_TYPES[authType].value,
|
||||
})),
|
||||
];
|
||||
|
||||
// Convert string array to Option array for MultiSelect
|
||||
const selectedOptions =
|
||||
(value
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
|
||||
// Convert default value to Option array
|
||||
const defaultOptions =
|
||||
(defaultValue
|
||||
?.map((val) => authOptions.find((option) => option.value === val))
|
||||
.filter(Boolean) as Option[]) || [];
|
||||
|
||||
const handleChange = (options: Option[]) => {
|
||||
const values = options.map((option) => option.value);
|
||||
onValueChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue placeholder={_(msg`Inherit authentication method`)} />
|
||||
<div className="relative">
|
||||
<MultiSelect
|
||||
value={selectedOptions}
|
||||
defaultOptions={defaultOptions}
|
||||
options={authOptions}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder || _(msg`Select authentication methods`)}
|
||||
className="bg-background text-muted-foreground"
|
||||
maxSelected={4} // Allow selecting up to 4 auth methods
|
||||
hideClearAllButton={false}
|
||||
/>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="-mr-1 ml-auto">
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md p-4">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient action authentication</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
<TooltipContent className="text-foreground max-w-md p-4">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Recipient action authentication</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>The authentication required for recipients to sign fields</Trans>
|
||||
</p>
|
||||
<p>
|
||||
<Trans>The authentication methods required for recipients to sign fields</Trans>
|
||||
</p>
|
||||
|
||||
<p className="mt-2">
|
||||
<Trans>This will override any global settings.</Trans>
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<Trans>
|
||||
These will override any global settings. Multiple methods can be selected.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Inherit authentication method</strong> - Use the global action signing
|
||||
authentication method configured in the "General Settings" step
|
||||
</Trans>
|
||||
</li>
|
||||
{/* <li>
|
||||
<strong>Require account</strong> - The recipient must be
|
||||
signed in
|
||||
</li> */}
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||
configured via their settings
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require 2FA</strong> - The recipient must have an account and 2FA enabled
|
||||
via their settings
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>None</strong> - No authentication required
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value="-1">
|
||||
<Trans>Inherit authentication method</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{Object.values(RecipientActionAuth)
|
||||
.filter((auth) => auth !== RecipientActionAuth.ACCOUNT)
|
||||
.map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Inherit authentication method</strong> - Use the global action signing
|
||||
authentication method configured in the "General Settings" step
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require passkey</strong> - The recipient must have an account and passkey
|
||||
configured via their settings
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>Require 2FA</strong> - The recipient must have an account and 2FA enabled
|
||||
via their settings
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
<strong>None</strong> - No authentication required
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -98,8 +98,8 @@ export const AddSettingsFormPartial = ({
|
||||
title: document.title,
|
||||
externalId: document.externalId || '',
|
||||
visibility: document.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
|
||||
meta: {
|
||||
timezone:
|
||||
@ -131,6 +131,12 @@ export const AddSettingsFormPartial = ({
|
||||
)
|
||||
.otherwise(() => false);
|
||||
|
||||
const onFormSubmit = form.handleSubmit(onSubmit);
|
||||
|
||||
const onGoNextClick = () => {
|
||||
void onFormSubmit().catch(console.error);
|
||||
};
|
||||
|
||||
// We almost always want to set the timezone to the user's local timezone to avoid confusion
|
||||
// when the document is signed.
|
||||
useEffect(() => {
|
||||
@ -214,7 +220,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@ -244,7 +254,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -286,7 +300,11 @@ export const AddSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthActionSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -370,7 +388,7 @@ export const AddSettingsFormPartial = ({
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
disabled={documentHasBeenSent}
|
||||
>
|
||||
@ -406,7 +424,7 @@ export const AddSettingsFormPartial = ({
|
||||
<Combobox
|
||||
className="bg-background"
|
||||
options={TIME_ZONES}
|
||||
{...field}
|
||||
value={field.value}
|
||||
onChange={(value) => value && field.onChange(value)}
|
||||
disabled={documentHasBeenSent}
|
||||
/>
|
||||
@ -461,7 +479,7 @@ export const AddSettingsFormPartial = ({
|
||||
disabled={form.formState.isSubmitting}
|
||||
canGoBack={stepIndex !== 0}
|
||||
onGoBackClick={previousStep}
|
||||
onGoNextClick={form.handleSubmit(onSubmit)}
|
||||
onGoNextClick={onGoNextClick}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</>
|
||||
|
||||
@ -16,17 +16,6 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
export const ZMapNegativeOneToUndefinedSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (val === '-1') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
export const ZAddSettingsFormSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
@ -34,12 +23,8 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
.min(1, { message: msg`Title cannot be empty`.id }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||
meta: z.object({
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
|
||||
@ -85,7 +85,7 @@ export const AddSignersFormPartial = ({
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 1,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
},
|
||||
];
|
||||
|
||||
@ -119,10 +119,14 @@ export const AddSignersFormPartial = ({
|
||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
return recipientAuthOptions?.accessAuth || recipientAuthOptions?.actionAuth;
|
||||
return (
|
||||
recipientAuthOptions.accessAuth.length > 0 || recipientAuthOptions.actionAuth.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const formHasActionAuth = form.getValues('signers').find((signer) => signer.actionAuth);
|
||||
const formHasActionAuth = form
|
||||
.getValues('signers')
|
||||
.find((signer) => signer.actionAuth.length > 0);
|
||||
|
||||
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
||||
}, [recipients, form]);
|
||||
@ -190,7 +194,7 @@ export const AddSignersFormPartial = ({
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
});
|
||||
};
|
||||
@ -226,7 +230,7 @@ export const AddSignersFormPartial = ({
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
@ -4,8 +4,6 @@ import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
||||
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -19,9 +17,7 @@ export const ZAddSignersFormSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
|
||||
@ -78,6 +78,8 @@ interface MultiSelectProps {
|
||||
>;
|
||||
/** hide the clear all button. */
|
||||
hideClearAllButton?: boolean;
|
||||
/** test id for the select value. */
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export interface MultiSelectRef {
|
||||
@ -170,6 +172,7 @@ const MultiSelect = ({
|
||||
commandProps,
|
||||
inputProps,
|
||||
hideClearAllButton = false,
|
||||
'data-testid': dataTestId,
|
||||
}: MultiSelectProps) => {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@ -403,6 +406,7 @@ const MultiSelect = ({
|
||||
commandProps?.shouldFilter !== undefined ? commandProps.shouldFilter : !onSearch
|
||||
} // When onSearch is provided, we don‘t want to filter the options. You can still override it.
|
||||
filter={commandFilter()}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -86,7 +86,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
{
|
||||
formId: initialId,
|
||||
role: RecipientRole.SIGNER,
|
||||
actionAuth: undefined,
|
||||
actionAuth: [],
|
||||
...generateRecipientPlaceholder(1),
|
||||
signingOrder: 1,
|
||||
},
|
||||
@ -136,10 +136,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
const recipientHasAuthOptions = recipients.find((recipient) => {
|
||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
return recipientAuthOptions?.accessAuth || recipientAuthOptions?.actionAuth;
|
||||
return (
|
||||
recipientAuthOptions.accessAuth.length > 0 || recipientAuthOptions.actionAuth.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const formHasActionAuth = form.getValues('signers').find((signer) => signer.actionAuth);
|
||||
const formHasActionAuth = form
|
||||
.getValues('signers')
|
||||
.find((signer) => signer.actionAuth.length > 0);
|
||||
|
||||
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
|
||||
}, [recipients, form]);
|
||||
@ -179,6 +183,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
email: user.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
actionAuth: [],
|
||||
});
|
||||
};
|
||||
|
||||
@ -188,6 +193,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
role: RecipientRole.SIGNER,
|
||||
...generateRecipientPlaceholder(placeholderRecipientCount),
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
actionAuth: [],
|
||||
});
|
||||
|
||||
setPlaceholderRecipientCount((count) => count + 1);
|
||||
|
||||
@ -3,8 +3,6 @@ import { z } from 'zod';
|
||||
|
||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
|
||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -15,9 +13,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
|
||||
@ -106,8 +106,8 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
title: template.title,
|
||||
externalId: template.externalId || undefined,
|
||||
visibility: template.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
meta: {
|
||||
subject: template.templateMeta?.subject ?? '',
|
||||
message: template.templateMeta?.message ?? '',
|
||||
@ -237,7 +237,11 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthAccessSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthAccessSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -378,7 +382,11 @@ export const AddTemplateSettingsFormPartial = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
<DocumentGlobalAuthActionSelect
|
||||
value={field.value}
|
||||
disabled={field.disabled}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -18,18 +18,12 @@ import {
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
} from '@documenso/trpc/server/document-router/schema';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
|
||||
export const ZAddTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||
meta: z.object({
|
||||
subject: z.string(),
|
||||
message: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user