mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
Merge branch 'main' into exp/autoplace-fields
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -57,6 +57,7 @@ export function AssistantConfirmationDialog({
|
||||
allowDictateNextSigner = false,
|
||||
defaultNextSigner,
|
||||
}: ConfirmationDialogProps) {
|
||||
const { t } = useLingui();
|
||||
const [isEditingNextSigner, setIsEditingNextSigner] = useState(false);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
@ -146,7 +147,7 @@ export function AssistantConfirmationDialog({
|
||||
<Input
|
||||
{...field}
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's name"
|
||||
placeholder={t`Enter the next signer's name`}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -168,7 +169,7 @@ export function AssistantConfirmationDialog({
|
||||
{...field}
|
||||
type="email"
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's email"
|
||||
placeholder={t`Enter the next signer's email`}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@ -126,7 +126,7 @@ export const EnvelopeDownloadDialog = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex w-full flex-col gap-4 overflow-hidden">
|
||||
{isLoadingEnvelopeItems ? (
|
||||
<>
|
||||
{Array.from({ length: 1 }).map((_, index) => (
|
||||
@ -159,7 +159,9 @@ export const EnvelopeDownloadDialog = ({
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Todo: Envelopes - Fix overflow */}
|
||||
<h4 className="text-foreground truncate text-sm font-medium">{item.title}</h4>
|
||||
<h4 className="text-foreground truncate text-sm font-medium" title={item.title}>
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||
<Trans>PDF Document</Trans>
|
||||
</p>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
@ -149,7 +148,7 @@ export const OrganisationEmailCreateDialog = ({
|
||||
<Trans>Display Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Support" />
|
||||
<Input {...field} placeholder={t`Support`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
@ -175,7 +174,7 @@ export const OrganisationEmailCreateDialog = ({
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value + '@' + emailDomain.domain);
|
||||
}}
|
||||
placeholder="support"
|
||||
placeholder={t`support`}
|
||||
/>
|
||||
<div className="bg-muted text-muted-foreground absolute bottom-0 right-0 top-0 flex items-center rounded-r-md border px-3 py-2 text-sm">
|
||||
@{emailDomain.domain}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
@ -134,7 +133,7 @@ export const OrganisationEmailUpdateDialog = ({
|
||||
<Trans>Display Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Support" />
|
||||
<Input {...field} placeholder={t`Support`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { Plural, Trans, useLingui } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
import { KeyRoundIcon } from 'lucide-react';
|
||||
@ -54,7 +52,7 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<TCreatePasskeyFormSchema>({
|
||||
@ -83,7 +81,7 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
|
||||
});
|
||||
|
||||
toast({
|
||||
description: _(msg`Successfully created passkey`),
|
||||
description: t`Successfully created passkey`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
@ -176,7 +174,7 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
|
||||
<Trans>Passkey name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" placeholder="eg. Mac" {...field} />
|
||||
<Input className="bg-background" placeholder={t`eg. Mac`} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -49,7 +47,7 @@ type TCreateTeamEmailFormSchema = z.infer<typeof ZCreateTeamEmailFormSchema>;
|
||||
export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
@ -73,8 +71,8 @@ export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDi
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`We have sent a confirmation email for verification.`),
|
||||
title: t`Success`,
|
||||
description: t`We have sent a confirmation email for verification.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
@ -87,17 +85,15 @@ export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDi
|
||||
if (error.code === AppErrorCode.ALREADY_EXISTS) {
|
||||
form.setError('email', {
|
||||
type: 'manual',
|
||||
message: _(msg`This email is already being used by another team.`),
|
||||
message: t`This email is already being used by another team.`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting to add this email. Please try again later.`,
|
||||
),
|
||||
title: t`An unknown error occurred`,
|
||||
description: t`We encountered an unknown error while attempting to add this email. Please try again later.`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -150,7 +146,7 @@ export const TeamEmailAddDialog = ({ teamId, trigger, ...props }: TeamEmailAddDi
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" placeholder="eg. Legal" {...field} />
|
||||
<Input className="bg-background" placeholder={t`eg. Legal`} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type { TeamEmail } from '@prisma/client';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -50,7 +48,7 @@ export const TeamEmailUpdateDialog = ({
|
||||
}: TeamEmailUpdateDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
@ -73,8 +71,8 @@ export const TeamEmailUpdateDialog = ({
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Team email was updated.`),
|
||||
title: t`Success`,
|
||||
description: t`Team email was updated.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
@ -83,10 +81,8 @@ export const TeamEmailUpdateDialog = ({
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting update the team email. Please try again later.`,
|
||||
),
|
||||
title: t`An unknown error occurred`,
|
||||
description: t`We encountered an unknown error while attempting update the team email. Please try again later.`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@ -138,7 +134,7 @@ export const TeamEmailUpdateDialog = ({
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" placeholder="eg. Legal" {...field} />
|
||||
<Input className="bg-background" placeholder={t`eg. Legal`} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type { Webhook } from '@prisma/client';
|
||||
import { WebhookTriggerEvents } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -52,7 +50,7 @@ const ZTestWebhookFormSchema = z.object({
|
||||
type TTestWebhookFormSchema = z.infer<typeof ZTestWebhookFormSchema>;
|
||||
|
||||
export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
@ -77,18 +75,16 @@ export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps)
|
||||
});
|
||||
|
||||
toast({
|
||||
title: _(msg`Test webhook sent`),
|
||||
description: _(msg`The test webhook has been successfully sent to your endpoint.`),
|
||||
title: t`Test webhook sent`,
|
||||
description: t`The test webhook has been successfully sent to your endpoint.`,
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: _(msg`Test webhook failed`),
|
||||
description: _(
|
||||
msg`We encountered an error while sending the test webhook. Please check your endpoint and try again.`,
|
||||
),
|
||||
title: t`Test webhook failed`,
|
||||
description: t`We encountered an error while sending the test webhook. Please check your endpoint and try again.`,
|
||||
variant: 'destructive',
|
||||
duration: 5000,
|
||||
});
|
||||
@ -129,7 +125,7 @@ export const WebhookTestDialog = ({ webhook, children }: WebhookTestDialogProps)
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an event type" />
|
||||
<SelectValue placeholder={t`Select an event type`} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { DocumentDistributionMethod } from '@prisma/client';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import type { Control } from 'react-hook-form';
|
||||
@ -45,7 +44,7 @@ export const ConfigureDocumentAdvancedSettings = ({
|
||||
control,
|
||||
isSubmitting,
|
||||
}: ConfigureDocumentAdvancedSettingsProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { t } = useLingui();
|
||||
|
||||
const form = useFormContext<TConfigureEmbedFormSchema>();
|
||||
const { features } = useConfigureDocument();
|
||||
@ -82,33 +81,6 @@ export const ConfigureDocumentAdvancedSettings = ({
|
||||
|
||||
<TabsContent value="general" className="mt-0">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* <FormField
|
||||
control={control}
|
||||
name="meta.externalId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>External ID</Trans>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<Trans>
|
||||
Add an external ID to the document. This can be used to identify the
|
||||
document in external systems.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} disabled={isSubmitting} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
|
||||
{features.allowConfigureSignatureTypes && (
|
||||
<FormField
|
||||
control={control}
|
||||
@ -121,13 +93,13 @@ export const ConfigureDocumentAdvancedSettings = ({
|
||||
<FormControl>
|
||||
<MultiSelectCombobox
|
||||
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
|
||||
label: _(option.label),
|
||||
label: t(option.label),
|
||||
value: option.value,
|
||||
}))}
|
||||
selectedValues={field.value}
|
||||
onChange={field.onChange}
|
||||
className="bg-background w-full"
|
||||
emptySelectionPlaceholder="Select signature types"
|
||||
emptySelectionPlaceholder={t`Select signature types`}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@ -66,14 +66,13 @@ export const ConfigureDocumentRecipients = ({
|
||||
});
|
||||
|
||||
const onAddSigner = useCallback(() => {
|
||||
const signerNumber = signers.length + 1;
|
||||
const recipientSigningOrder =
|
||||
signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1;
|
||||
|
||||
appendSigner({
|
||||
formId: nanoid(8),
|
||||
name: isTemplate ? `Recipient ${signerNumber}` : '',
|
||||
email: isTemplate ? `recipient.${signerNumber}@document.com` : '',
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder:
|
||||
signingOrder === DocumentSigningOrder.SEQUENTIAL ? recipientSigningOrder : undefined,
|
||||
|
||||
@ -25,9 +25,11 @@ import { ConfigureDocumentUpload } from './configure-document-upload';
|
||||
import {
|
||||
type TConfigureEmbedFormSchema,
|
||||
ZConfigureEmbedFormSchema,
|
||||
ZConfigureTemplateEmbedFormSchema,
|
||||
} from './configure-document-view.types';
|
||||
|
||||
export interface ConfigureDocumentViewProps {
|
||||
type?: 'document' | 'template';
|
||||
onSubmit: (data: TConfigureEmbedFormSchema) => void | Promise<void>;
|
||||
defaultValues?: Partial<TConfigureEmbedFormSchema>;
|
||||
disableUpload?: boolean;
|
||||
@ -35,6 +37,7 @@ export interface ConfigureDocumentViewProps {
|
||||
}
|
||||
|
||||
export const ConfigureDocumentView = ({
|
||||
type = 'document',
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
disableUpload,
|
||||
@ -42,14 +45,16 @@ export const ConfigureDocumentView = ({
|
||||
const { isTemplate } = useConfigureDocument();
|
||||
|
||||
const form = useForm<TConfigureEmbedFormSchema>({
|
||||
resolver: zodResolver(ZConfigureEmbedFormSchema),
|
||||
resolver: zodResolver(
|
||||
type === 'template' ? ZConfigureTemplateEmbedFormSchema : ZConfigureEmbedFormSchema,
|
||||
),
|
||||
defaultValues: {
|
||||
title: defaultValues?.title || '',
|
||||
signers: defaultValues?.signers || [
|
||||
{
|
||||
formId: nanoid(8),
|
||||
name: isTemplate ? `Recipient ${1}` : '',
|
||||
email: isTemplate ? `recipient.${1}@document.com` : '',
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: 1,
|
||||
disabled: false,
|
||||
|
||||
@ -17,7 +17,7 @@ export const ZConfigureEmbedFormSchema = z.object({
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
formId: z.string(),
|
||||
name: z.string().min(1, { message: 'Name is required' }),
|
||||
name: z.string(),
|
||||
email: z.string().email('Invalid email address'),
|
||||
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||
signingOrder: z.number().optional(),
|
||||
@ -48,3 +48,17 @@ export const ZConfigureEmbedFormSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZConfigureTemplateEmbedFormSchema = ZConfigureEmbedFormSchema.extend({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
formId: z.string(),
|
||||
name: z.string(),
|
||||
email: z.union([z.string().length(0), z.string().email('Invalid email address')]),
|
||||
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||
signingOrder: z.number().optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@ -42,7 +42,7 @@ export type ConfigureFieldsViewProps = {
|
||||
configData: TConfigureEmbedFormSchema;
|
||||
documentData?: DocumentData;
|
||||
defaultValues?: Partial<TConfigureFieldsFormSchema>;
|
||||
onBack: (data: TConfigureFieldsFormSchema) => void;
|
||||
onBack?: (data: TConfigureFieldsFormSchema) => void;
|
||||
onSubmit: (data: TConfigureFieldsFormSchema) => void;
|
||||
};
|
||||
|
||||
@ -481,15 +481,17 @@ export const ConfigureFieldsView = ({
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex-1"
|
||||
loading={form.formState.isSubmitting}
|
||||
onClick={() => onBack(form.getValues())}
|
||||
>
|
||||
<Trans>Back</Trans>
|
||||
</Button>
|
||||
{onBack && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex-1"
|
||||
loading={form.formState.isSubmitting}
|
||||
onClick={() => onBack(form.getValues())}
|
||||
>
|
||||
<Trans>Back</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="flex-1"
|
||||
@ -642,15 +644,17 @@ export const ConfigureFieldsView = ({
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex-1"
|
||||
loading={form.formState.isSubmitting}
|
||||
onClick={() => onBack(form.getValues())}
|
||||
>
|
||||
<Trans>Back</Trans>
|
||||
</Button>
|
||||
{onBack && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex-1"
|
||||
loading={form.formState.isSubmitting}
|
||||
onClick={() => onBack(form.getValues())}
|
||||
>
|
||||
<Trans>Back</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="flex-1"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
@ -46,6 +46,8 @@ export const DocumentSigningAuthPassword = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DocumentSigningAuthPasswordProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { recipient, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||
useRequiredDocumentSigningAuthContext();
|
||||
|
||||
@ -120,7 +122,7 @@ export const DocumentSigningAuthPassword = ({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
placeholder={t`Enter your password`}
|
||||
{...field}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useSearchParams } from 'react-router';
|
||||
@ -48,6 +48,7 @@ export function DocumentSigningRejectDialog({
|
||||
onRejected,
|
||||
trigger,
|
||||
}: DocumentSigningRejectDialogProps) {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
@ -141,7 +142,7 @@ export function DocumentSigningRejectDialog({
|
||||
<Textarea
|
||||
{...field}
|
||||
rows={4}
|
||||
placeholder="Please provide a reason for rejecting this document"
|
||||
placeholder={t`Please provide a reason for rejecting this document`}
|
||||
disabled={form.formState.isSubmitting}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@ -5,7 +5,7 @@ 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 } from 'lucide-react';
|
||||
import { CopyPlusIcon, SquareStackIcon, TrashIcon, UserCircleIcon } from 'lucide-react';
|
||||
|
||||
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
|
||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||
@ -24,8 +24,10 @@ 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 { fieldButtonList } from './envelope-editor-fields-drag-drop';
|
||||
import { EnvelopeRecipientSelectorCommand } from './envelope-recipient-selector';
|
||||
|
||||
export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
const { t, i18n } = useLingui();
|
||||
@ -490,6 +492,18 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
setSelectedFields([]);
|
||||
};
|
||||
|
||||
const changeSelectedFieldsRecipients = (recipientId: number) => {
|
||||
const fields = selectedKonvaFieldGroups
|
||||
.map((field) => editorFields.getFieldByFormId(field.id()))
|
||||
.filter((field) => field !== undefined);
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.recipientId !== recipientId) {
|
||||
editorFields.updateFieldByFormId(field.formId, { recipientId, id: undefined });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const duplicatedSelectedFields = () => {
|
||||
const fields = selectedKonvaFieldGroups
|
||||
.map((field) => editorFields.getFieldByFormId(field.id()))
|
||||
@ -574,7 +588,12 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
{selectedKonvaFieldGroups.length > 0 &&
|
||||
interactiveTransformer.current &&
|
||||
!isFieldChanging && (
|
||||
<div
|
||||
<FieldActionButtons
|
||||
handleDuplicateSelectedFields={duplicatedSelectedFields}
|
||||
handleDuplicateSelectedFieldsOnAllPages={duplicatedSelectedFieldsOnAllPages}
|
||||
handleDeleteSelectedFields={deletedSelectedFields}
|
||||
handleChangeRecipient={changeSelectedFieldsRecipients}
|
||||
selectedFieldFormId={selectedKonvaFieldGroups.map((field) => field.id())}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top:
|
||||
@ -591,35 +610,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 50,
|
||||
}}
|
||||
className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5"
|
||||
>
|
||||
<button
|
||||
title={t`Duplicate`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={() => duplicatedSelectedFields()}
|
||||
onTouchEnd={() => duplicatedSelectedFields()}
|
||||
>
|
||||
<CopyPlusIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<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={() => duplicatedSelectedFieldsOnAllPages()}
|
||||
onTouchEnd={() => duplicatedSelectedFieldsOnAllPages()}
|
||||
>
|
||||
<SquareStackIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
title={t`Remove`}
|
||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||
onClick={() => deletedSelectedFields()}
|
||||
onTouchEnd={() => deletedSelectedFields()}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
/>
|
||||
)}
|
||||
|
||||
{pendingFieldCreation && (
|
||||
@ -666,3 +657,119 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type FieldActionButtonsProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
handleDuplicateSelectedFields: () => void;
|
||||
handleDuplicateSelectedFieldsOnAllPages: () => void;
|
||||
handleDeleteSelectedFields: () => void;
|
||||
handleChangeRecipient: (recipientId: number) => void;
|
||||
selectedFieldFormId: string[];
|
||||
};
|
||||
|
||||
const FieldActionButtons = ({
|
||||
handleDuplicateSelectedFields,
|
||||
handleDuplicateSelectedFieldsOnAllPages,
|
||||
handleDeleteSelectedFields,
|
||||
handleChangeRecipient,
|
||||
selectedFieldFormId,
|
||||
...props
|
||||
}: FieldActionButtonsProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [showRecipientSelector, setShowRecipientSelector] = useState(false);
|
||||
|
||||
const { editorFields, envelope } = useCurrentEnvelopeEditor();
|
||||
|
||||
/**
|
||||
* Decide the preselected recipient in the command input.
|
||||
*
|
||||
* If all fields belong to the same recipient then use that recipient as the default.
|
||||
*
|
||||
* Otherwise show the placeholder.
|
||||
*/
|
||||
const preselectedRecipient = useMemo(() => {
|
||||
if (selectedFieldFormId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = editorFields.localFields.filter((field) =>
|
||||
selectedFieldFormId.includes(field.formId),
|
||||
);
|
||||
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === fields[0].recipientId,
|
||||
);
|
||||
|
||||
if (!recipient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRecipientsSame = fields.every((field) => field.recipientId === recipient.id);
|
||||
|
||||
if (isRecipientsSame) {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [editorFields.localFields]);
|
||||
|
||||
return (
|
||||
<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
|
||||
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)}
|
||||
onTouchEnd={() => setShowRecipientSelector(true)}
|
||||
>
|
||||
<UserCircleIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<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}
|
||||
onTouchEnd={handleDuplicateSelectedFields}
|
||||
>
|
||||
<CopyPlusIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<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}
|
||||
onTouchEnd={handleDuplicateSelectedFieldsOnAllPages}
|
||||
>
|
||||
<SquareStackIcon className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<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}
|
||||
onTouchEnd={handleDeleteSelectedFields}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CommandDialog
|
||||
position="start"
|
||||
open={showRecipientSelector}
|
||||
onOpenChange={setShowRecipientSelector}
|
||||
>
|
||||
<EnvelopeRecipientSelectorCommand
|
||||
placeholder={t`Select a recipient`}
|
||||
selectedRecipient={preselectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) => {
|
||||
editorFields.setSelectedRecipient(recipient.id);
|
||||
handleChangeRecipient(recipient.id);
|
||||
setShowRecipientSelector(false);
|
||||
}}
|
||||
recipients={envelope.recipients}
|
||||
fields={envelope.fields}
|
||||
/>
|
||||
</CommandDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import { msg, plural } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { FieldType, RecipientRole } from '@prisma/client';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { isDeepEqual } from 'remeda';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -32,7 +32,6 @@ import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animat
|
||||
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
|
||||
import { Separator } from '@documenso/ui/primitives/separator';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@ -49,6 +48,7 @@ import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text
|
||||
|
||||
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
|
||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||
import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
|
||||
|
||||
const EnvelopeEditorFieldsPageRenderer = lazy(
|
||||
async () => import('./envelope-editor-fields-page-renderer'),
|
||||
@ -171,6 +171,8 @@ const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
|
||||
};
|
||||
|
||||
export const EnvelopeEditorFieldsPage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
@ -407,12 +409,13 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
<Trans>Selected Recipient</Trans>
|
||||
</h3>
|
||||
|
||||
<RecipientSelector
|
||||
<EnvelopeRecipientSelector
|
||||
selectedRecipient={editorFields.selectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) =>
|
||||
editorFields.setSelectedRecipient(recipient.id)
|
||||
}
|
||||
recipients={envelope.recipients}
|
||||
fields={envelope.fields}
|
||||
className="w-full"
|
||||
align="end"
|
||||
/>
|
||||
@ -591,6 +594,37 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
<section>
|
||||
<Separator className="my-4" />
|
||||
|
||||
{searchParams.get('devmode') && (
|
||||
<>
|
||||
<div className="px-4">
|
||||
<h3 className="text-foreground mb-3 text-sm font-semibold">
|
||||
<Trans>Developer Mode</Trans>
|
||||
</h3>
|
||||
|
||||
<div className="bg-muted/50 border-border text-foreground space-y-2 rounded-md border p-3 text-sm">
|
||||
<p>
|
||||
<span className="text-muted-foreground min-w-12">Pos X: </span>
|
||||
{selectedField.positionX.toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground min-w-12">Pos Y: </span>
|
||||
{selectedField.positionY.toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground min-w-12">Width: </span>
|
||||
{selectedField.width.toFixed(2)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-muted-foreground min-w-12">Height: </span>
|
||||
{selectedField.height.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="[&_label]:text-foreground/70 px-4 [&_label]:text-xs">
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t(FieldSettingsTypeTranslations[selectedField.type])}
|
||||
|
||||
@ -233,7 +233,19 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
const canUpdateVisibility = canAccessTeamDocument(team.currentTeamRole, envelope.visibility);
|
||||
|
||||
const onFormSubmit = async (data: TAddSettingsFormSchema) => {
|
||||
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
|
||||
const {
|
||||
timezone,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
language,
|
||||
signatureTypes,
|
||||
distributionMethod,
|
||||
emailId,
|
||||
emailSettings,
|
||||
message,
|
||||
subject,
|
||||
emailReplyTo,
|
||||
} = data.meta;
|
||||
|
||||
const parsedGlobalAccessAuth = z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
@ -251,10 +263,16 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
timezone,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
emailId,
|
||||
message,
|
||||
subject,
|
||||
emailReplyTo,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
language: isValidLanguageCode(language) ? language : undefined,
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,252 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
||||
import { sortBy } from 'remeda';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
||||
import { getRecipientColorStyles } from '@documenso/ui/lib/recipient-colors';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from '@documenso/ui/primitives/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export interface EnvelopeRecipientSelectorProps {
|
||||
className?: string;
|
||||
selectedRecipient: Recipient | null;
|
||||
onSelectedRecipientChange: (recipient: Recipient) => void;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
align?: 'center' | 'end' | 'start';
|
||||
}
|
||||
|
||||
export const EnvelopeRecipientSelector = ({
|
||||
className,
|
||||
selectedRecipient,
|
||||
onSelectedRecipientChange,
|
||||
recipients,
|
||||
fields,
|
||||
align = 'start',
|
||||
}: EnvelopeRecipientSelectorProps) => {
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||
0,
|
||||
),
|
||||
).comboxBoxTrigger,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedRecipient?.email && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedRecipient?.name} ({selectedRecipient?.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!selectedRecipient?.email && (
|
||||
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-0" align={align}>
|
||||
<EnvelopeRecipientSelectorCommand
|
||||
fields={fields}
|
||||
selectedRecipient={selectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) => {
|
||||
onSelectedRecipientChange(recipient);
|
||||
setShowRecipientsSelector(false);
|
||||
}}
|
||||
recipients={recipients}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
interface EnvelopeRecipientSelectorCommandProps {
|
||||
className?: string;
|
||||
selectedRecipient: Recipient | null;
|
||||
onSelectedRecipientChange: (recipient: Recipient) => void;
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const EnvelopeRecipientSelectorCommand = ({
|
||||
className,
|
||||
selectedRecipient,
|
||||
onSelectedRecipientChange,
|
||||
recipients,
|
||||
fields,
|
||||
placeholder,
|
||||
}: EnvelopeRecipientSelectorCommandProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const recipientsByRole = useCallback(() => {
|
||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||
CC: [],
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
APPROVER: [],
|
||||
ASSISTANT: [],
|
||||
};
|
||||
|
||||
recipients.forEach((recipient) => {
|
||||
recipientsByRole[recipient.role].push(recipient);
|
||||
});
|
||||
|
||||
return recipientsByRole;
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRoleToDisplay = useCallback(() => {
|
||||
return Object.entries(recipientsByRole())
|
||||
.filter(
|
||||
([role]) =>
|
||||
role !== RecipientRole.CC &&
|
||||
role !== RecipientRole.VIEWER &&
|
||||
role !== RecipientRole.ASSISTANT,
|
||||
)
|
||||
.map(
|
||||
([role, roleRecipients]) =>
|
||||
[
|
||||
role,
|
||||
sortBy(
|
||||
roleRecipients,
|
||||
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
|
||||
[(r) => r.id, 'asc'],
|
||||
),
|
||||
] as [RecipientRole, Recipient[]],
|
||||
);
|
||||
}, [recipientsByRole]);
|
||||
|
||||
const isRecipientDisabled = useCallback(
|
||||
(recipientId: number) => {
|
||||
const recipient = recipients.find((r) => r.id === recipientId);
|
||||
const recipientFields = fields.filter((f) => f.recipientId === recipientId);
|
||||
|
||||
return !recipient || !canRecipientFieldsBeModified(recipient, recipientFields);
|
||||
},
|
||||
[fields, recipients],
|
||||
);
|
||||
|
||||
return (
|
||||
<Command
|
||||
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
||||
className={className}
|
||||
>
|
||||
<CommandInput placeholder={placeholder} />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
</div>
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{roleRecipients.map((recipient) => (
|
||||
<CommandItem
|
||||
key={recipient.id}
|
||||
className={cn(
|
||||
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === recipient.id),
|
||||
0,
|
||||
),
|
||||
).comboxBoxItem,
|
||||
{
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
'cursor-not-allowed': isRecipientDisabled(recipient.id),
|
||||
},
|
||||
)}
|
||||
onSelect={() => {
|
||||
if (!isRecipientDisabled(recipient.id)) {
|
||||
onSelectedRecipientChange(recipient);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||
'opacity-50': isRecipientDisabled(recipient.id),
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center justify-center">
|
||||
{!isRecipientDisabled(recipient.id) ? (
|
||||
<Check
|
||||
aria-hidden={recipient.id !== selectedRecipient?.id}
|
||||
className={cn('h-4 w-4 flex-shrink-0', {
|
||||
'opacity-0': recipient.id !== selectedRecipient?.id,
|
||||
'opacity-100': recipient.id === selectedRecipient?.id,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger disabled={false}>
|
||||
<Info className="z-50 ml-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<Trans>
|
||||
This document has already been sent to this recipient. You can no longer
|
||||
edit this recipient.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</Command>
|
||||
);
|
||||
};
|
||||
@ -282,18 +282,6 @@ export const OrgMenuSwitcher = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/inbox">
|
||||
<Trans>Personal Inbox</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/settings/profile">
|
||||
<Trans>Account</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{currentOrganisation &&
|
||||
canExecuteOrganisationAction(
|
||||
'MANAGE_ORGANISATION',
|
||||
@ -314,6 +302,18 @@ export const OrgMenuSwitcher = () => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/inbox">
|
||||
<Trans>Personal Inbox</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/settings/profile">
|
||||
<Trans>Account</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground px-4 py-2"
|
||||
onClick={() => setLanguageSwitcherOpen(true)}
|
||||
|
||||
@ -3,6 +3,8 @@ import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -15,6 +17,8 @@ export type OrganisationBillingPortalButtonProps = {
|
||||
export const OrganisationBillingPortalButton = ({
|
||||
buttonProps,
|
||||
}: OrganisationBillingPortalButtonProps) => {
|
||||
const { organisations } = useSession();
|
||||
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const { _ } = useLingui();
|
||||
@ -30,7 +34,10 @@ export const OrganisationBillingPortalButton = ({
|
||||
|
||||
const handleCreatePortal = async () => {
|
||||
try {
|
||||
const { redirectUrl } = await manageSubscription({ organisationId: organisation.id });
|
||||
const { redirectUrl } = await manageSubscription({
|
||||
organisationId: organisation.id,
|
||||
isPersonalLayoutMode: isPersonalLayout(organisations),
|
||||
});
|
||||
|
||||
window.open(redirectUrl, '_blank');
|
||||
} catch (err) {
|
||||
|
||||
@ -16,7 +16,7 @@ import { Link } from 'react-router';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
@ -29,6 +29,10 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const hasManageableBillingOrgs = organisations.some((org) =>
|
||||
canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-y-2', className)} {...props}>
|
||||
<Link to="/settings/profile">
|
||||
@ -127,21 +131,6 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr
|
||||
<Trans>Webhooks</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link to="/settings/billing">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/billing') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<CreditCardIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Billing</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -158,6 +147,21 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && hasManageableBillingOrgs && (
|
||||
<Link to={isPersonalLayoutMode ? '/settings/billing-personal' : `/settings/billing`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/billing') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<CreditCardIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Billing</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link to="/settings/security">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@ -17,7 +17,7 @@ import { Link, useLocation } from 'react-router';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
@ -30,6 +30,10 @@ export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProp
|
||||
|
||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||
|
||||
const hasManageableBillingOrgs = organisations.some((org) =>
|
||||
canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-wrap items-center justify-start gap-x-2 gap-y-4', className)}
|
||||
@ -127,21 +131,6 @@ export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProp
|
||||
<Trans>Webhooks</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link to="/settings/billing">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/billing') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<CreditCardIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Billing</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -158,6 +147,21 @@ export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProp
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && hasManageableBillingOrgs && (
|
||||
<Link to={isPersonalLayoutMode ? '/settings/billing-personal' : `/settings/billing`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/billing') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<CreditCardIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Billing</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link to="/settings/security">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@ -191,7 +191,7 @@ export const SettingsSecurityPasskeyTableActions = ({
|
||||
|
||||
<DialogDescription className="mt-4">
|
||||
<Trans>
|
||||
Are you sure you want to remove the <strong>{passkeyName}</strong> passkey.
|
||||
Are you sure you want to remove the <strong>{passkeyName}</strong> passkey?
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
|
||||
export const UserBillingOrganisationsTable = () => {
|
||||
const { t } = useLingui();
|
||||
const { organisations } = useSession();
|
||||
|
||||
const billingOrganisations = useMemo(() => {
|
||||
return organisations.filter((org) =>
|
||||
canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole),
|
||||
);
|
||||
}, [organisations]);
|
||||
|
||||
const getSubscriptionStatusDisplay = (status: SubscriptionStatus | undefined) => {
|
||||
return match(status)
|
||||
.with(SubscriptionStatus.ACTIVE, () => ({
|
||||
label: t`Active`,
|
||||
variant: 'default' as const,
|
||||
}))
|
||||
.with(SubscriptionStatus.PAST_DUE, () => ({
|
||||
label: t`Past Due`,
|
||||
variant: 'warning' as const,
|
||||
}))
|
||||
.with(SubscriptionStatus.INACTIVE, () => ({
|
||||
label: t`Inactive`,
|
||||
variant: 'neutral' as const,
|
||||
}))
|
||||
.otherwise(() => ({
|
||||
label: t`Free`,
|
||||
variant: 'neutral' as const,
|
||||
}));
|
||||
};
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
header: t`Organisation`,
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => (
|
||||
<Link to={`/o/${row.original.url}`} preventScrollReset={true}>
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(row.original.avatarImageId)}
|
||||
avatarClass="h-12 w-12"
|
||||
avatarFallback={row.original.name.slice(0, 1).toUpperCase()}
|
||||
primaryText={
|
||||
<span className="text-foreground/80 font-semibold">{row.original.name}</span>
|
||||
}
|
||||
secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/o/${row.original.url}`}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t`Subscription Status`,
|
||||
accessorKey: 'subscription',
|
||||
cell: ({ row }) => {
|
||||
const subscription = row.original.subscription;
|
||||
const status = subscription?.status;
|
||||
const { label, variant } = getSubscriptionStatusDisplay(status);
|
||||
|
||||
return <Badge variant={variant}>{label}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: t`Actions`,
|
||||
id: 'actions',
|
||||
cell: ({ row }) => (
|
||||
<Button asChild variant="outline">
|
||||
<Link to={`/o/${row.original.url}/settings/billing`}>
|
||||
<Trans>Manage Billing</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof billingOrganisations)[number]>[];
|
||||
}, [billingOrganisations]);
|
||||
|
||||
if (billingOrganisations.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center">
|
||||
<p className="text-sm">
|
||||
<Trans>You don't manage billing for any organisations.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={billingOrganisations}
|
||||
perPage={billingOrganisations.length}
|
||||
currentPage={1}
|
||||
totalPages={1}
|
||||
/>
|
||||
);
|
||||
};
|
||||
24
apps/remix/app/routes/_authenticated+/settings+/billing.tsx
Normal file
24
apps/remix/app/routes/_authenticated+/settings+/billing.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { UserBillingOrganisationsTable } from '~/components/tables/user-billing-organisations-table';
|
||||
import { appMetaTags } from '~/utils/meta';
|
||||
|
||||
export function meta() {
|
||||
return appMetaTags('Billing');
|
||||
}
|
||||
|
||||
export default function SettingsBilling() {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title={t`Billing`}
|
||||
subtitle={t`Manage billing and subscriptions for organisations where you have billing management permissions.`}
|
||||
/>
|
||||
|
||||
<UserBillingOrganisationsTable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -36,14 +36,14 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
}
|
||||
|
||||
return {
|
||||
hasValidToken: !!result,
|
||||
token,
|
||||
hasValidToken: !!result,
|
||||
allowEmbedAuthoringWhiteLabel,
|
||||
};
|
||||
};
|
||||
|
||||
export default function AuthoringLayout() {
|
||||
const { hasValidToken, token, allowEmbedAuthoringWhiteLabel } = useLoaderData<typeof loader>();
|
||||
const { token, hasValidToken, allowEmbedAuthoringWhiteLabel } = useLoaderData<typeof loader>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
|
||||
@ -27,7 +27,7 @@ import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fiel
|
||||
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
|
||||
import {
|
||||
type TBaseEmbedAuthoringSchema,
|
||||
ZBaseEmbedAuthoringSchema,
|
||||
ZBaseEmbedAuthoringEditSchema,
|
||||
} from '~/types/embed-authoring-base-schema';
|
||||
|
||||
import type { Route } from './+types/document.edit.$id';
|
||||
@ -88,6 +88,8 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
||||
|
||||
const { document } = useLoaderData<typeof loader>();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
|
||||
const signatureTypes = useMemo(() => {
|
||||
const types: string[] = [];
|
||||
|
||||
@ -159,6 +161,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
||||
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
|
||||
const [externalId, setExternalId] = useState<string | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [canGoBack, setCanGoBack] = useState(true);
|
||||
|
||||
const { mutateAsync: updateEmbeddingDocument } =
|
||||
trpc.embeddingPresign.updateEmbeddingDocument.useMutation();
|
||||
@ -177,6 +180,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
||||
fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)),
|
||||
};
|
||||
});
|
||||
|
||||
setCurrentStep(2);
|
||||
};
|
||||
|
||||
@ -275,7 +279,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||
const result = ZBaseEmbedAuthoringEditSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
@ -285,15 +289,26 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
||||
|
||||
setFeatures(result.data.features);
|
||||
|
||||
if (result.data.onlyEditFields) {
|
||||
setCurrentStep(2);
|
||||
setCanGoBack(false);
|
||||
}
|
||||
|
||||
// Extract externalId from the parsed data if available
|
||||
if (result.data.externalId) {
|
||||
setExternalId(result.data.externalId);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
} catch (err) {
|
||||
console.error('Error parsing embedding params:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!hasFinishedInit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6">
|
||||
<ConfigureDocumentProvider isTemplate={false} features={features ?? {}}>
|
||||
@ -308,7 +323,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
|
||||
configData={configuration!}
|
||||
documentData={document.documentData}
|
||||
defaultValues={fields ?? undefined}
|
||||
onBack={handleBackToConfig}
|
||||
onBack={canGoBack ? handleBackToConfig : undefined}
|
||||
onSubmit={handleConfigureFieldsSubmit}
|
||||
/>
|
||||
</Stepper>
|
||||
|
||||
@ -154,6 +154,7 @@ export default function EmbeddingAuthoringTemplateCreatePage() {
|
||||
<ConfigureDocumentProvider isTemplate={true} features={features ?? {}}>
|
||||
<Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}>
|
||||
<ConfigureDocumentView
|
||||
type="template"
|
||||
defaultValues={configuration ?? undefined}
|
||||
onSubmit={handleConfigurePageViewSubmit}
|
||||
/>
|
||||
|
||||
@ -27,7 +27,7 @@ import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fiel
|
||||
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
|
||||
import {
|
||||
type TBaseEmbedAuthoringSchema,
|
||||
ZBaseEmbedAuthoringSchema,
|
||||
ZBaseEmbedAuthoringEditSchema,
|
||||
} from '~/types/embed-authoring-base-schema';
|
||||
|
||||
import type { Route } from './+types/document.edit.$id';
|
||||
@ -88,6 +88,8 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
||||
|
||||
const { template } = useLoaderData<typeof loader>();
|
||||
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
|
||||
const signatureTypes = useMemo(() => {
|
||||
const types: string[] = [];
|
||||
|
||||
@ -159,6 +161,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
||||
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
|
||||
const [externalId, setExternalId] = useState<string | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [canGoBack, setCanGoBack] = useState(true);
|
||||
|
||||
const { mutateAsync: updateEmbeddingTemplate } =
|
||||
trpc.embeddingPresign.updateEmbeddingTemplate.useMutation();
|
||||
@ -230,7 +233,9 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
||||
signingOrder: signer.signingOrder,
|
||||
fields: fields
|
||||
.filter((field) => field.signerEmail === signer.email)
|
||||
.map((f) => ({
|
||||
// There's a gnarly discriminated union that makes this hard to satisfy, we're casting for the second
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map<any>((f) => ({
|
||||
...f,
|
||||
id: f.nativeId,
|
||||
envelopeItemId: template.templateDocumentData.envelopeItemId,
|
||||
@ -273,7 +278,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
||||
try {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||
const result = ZBaseEmbedAuthoringEditSchema.safeParse(
|
||||
JSON.parse(decodeURIComponent(atob(hash))),
|
||||
);
|
||||
|
||||
@ -283,20 +288,32 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
||||
|
||||
setFeatures(result.data.features);
|
||||
|
||||
if (result.data.onlyEditFields) {
|
||||
setCurrentStep(2);
|
||||
setCanGoBack(false);
|
||||
}
|
||||
|
||||
// Extract externalId from the parsed data if available
|
||||
if (result.data.externalId) {
|
||||
setExternalId(result.data.externalId);
|
||||
}
|
||||
|
||||
setHasFinishedInit(true);
|
||||
} catch (err) {
|
||||
console.error('Error parsing embedding params:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!hasFinishedInit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg p-6">
|
||||
<ConfigureDocumentProvider isTemplate={false} features={features ?? {}}>
|
||||
<Stepper currentStep={currentStep} setCurrentStep={setCurrentStep}>
|
||||
<ConfigureDocumentView
|
||||
type="template"
|
||||
defaultValues={configuration ?? undefined}
|
||||
disableUpload={true}
|
||||
onSubmit={handleConfigurePageViewSubmit}
|
||||
@ -306,7 +323,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
|
||||
configData={configuration!}
|
||||
documentData={template.templateDocumentData}
|
||||
defaultValues={fields ?? undefined}
|
||||
onBack={handleBackToConfig}
|
||||
onBack={canGoBack ? handleBackToConfig : undefined}
|
||||
onSubmit={handleConfigureFieldsSubmit}
|
||||
/>
|
||||
</Stepper>
|
||||
|
||||
@ -2,21 +2,24 @@ import { z } from 'zod';
|
||||
|
||||
import { ZBaseEmbedDataSchema } from './embed-base-schemas';
|
||||
|
||||
export const ZBaseEmbedAuthoringSchema = z
|
||||
.object({
|
||||
externalId: z.string().optional(),
|
||||
features: z
|
||||
.object({
|
||||
allowConfigureSignatureTypes: z.boolean().optional(),
|
||||
allowConfigureLanguage: z.boolean().optional(),
|
||||
allowConfigureDateFormat: z.boolean().optional(),
|
||||
allowConfigureTimezone: z.boolean().optional(),
|
||||
allowConfigureRedirectUrl: z.boolean().optional(),
|
||||
allowConfigureCommunication: z.boolean().optional(),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
})
|
||||
.and(ZBaseEmbedDataSchema);
|
||||
export const ZBaseEmbedAuthoringSchema = ZBaseEmbedDataSchema.extend({
|
||||
externalId: z.string().optional(),
|
||||
features: z
|
||||
.object({
|
||||
allowConfigureSignatureTypes: z.boolean().optional(),
|
||||
allowConfigureLanguage: z.boolean().optional(),
|
||||
allowConfigureDateFormat: z.boolean().optional(),
|
||||
allowConfigureTimezone: z.boolean().optional(),
|
||||
allowConfigureRedirectUrl: z.boolean().optional(),
|
||||
allowConfigureCommunication: z.boolean().optional(),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export const ZBaseEmbedAuthoringEditSchema = ZBaseEmbedAuthoringSchema.extend({
|
||||
onlyEditFields: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type TBaseEmbedAuthoringSchema = z.infer<typeof ZBaseEmbedAuthoringSchema>;
|
||||
export type TBaseEmbedAuthoringEditSchema = z.infer<typeof ZBaseEmbedAuthoringEditSchema>;
|
||||
|
||||
Reference in New Issue
Block a user