mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: embed authoring part two (#1768)
This commit is contained in:
@ -57,7 +57,7 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
name: 'signers',
|
name: 'signers',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getValues, watch } = useFormContext<TConfigureEmbedFormSchema>();
|
const { getValues, watch, setValue } = useFormContext<TConfigureEmbedFormSchema>();
|
||||||
|
|
||||||
const signingOrder = watch('meta.signingOrder');
|
const signingOrder = watch('meta.signingOrder');
|
||||||
|
|
||||||
@ -67,13 +67,16 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
|
|
||||||
const onAddSigner = useCallback(() => {
|
const onAddSigner = useCallback(() => {
|
||||||
const signerNumber = signers.length + 1;
|
const signerNumber = signers.length + 1;
|
||||||
|
const recipientSigningOrder =
|
||||||
|
signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1;
|
||||||
|
|
||||||
appendSigner({
|
appendSigner({
|
||||||
formId: nanoid(8),
|
formId: nanoid(8),
|
||||||
name: isTemplate ? `Recipient ${signerNumber}` : '',
|
name: isTemplate ? `Recipient ${signerNumber}` : '',
|
||||||
email: isTemplate ? `recipient.${signerNumber}@document.com` : '',
|
email: isTemplate ? `recipient.${signerNumber}@document.com` : '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder || 0) + 1 : 1,
|
signingOrder:
|
||||||
|
signingOrder === DocumentSigningOrder.SEQUENTIAL ? recipientSigningOrder : undefined,
|
||||||
});
|
});
|
||||||
}, [appendSigner, signers]);
|
}, [appendSigner, signers]);
|
||||||
|
|
||||||
@ -103,7 +106,7 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
// Update signing order for each item
|
// Update signing order for each item
|
||||||
const updatedSigners = remainingSigners.map((s: SignerItem, idx: number) => ({
|
const updatedSigners = remainingSigners.map((s: SignerItem, idx: number) => ({
|
||||||
...s,
|
...s,
|
||||||
signingOrder: idx + 1,
|
signingOrder: signingOrder === DocumentSigningOrder.SEQUENTIAL ? idx + 1 : undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update the form
|
// Update the form
|
||||||
@ -123,7 +126,7 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
const currentSigners = getValues('signers');
|
const currentSigners = getValues('signers');
|
||||||
const updatedSigners = currentSigners.map((signer: SignerItem, index: number) => ({
|
const updatedSigners = currentSigners.map((signer: SignerItem, index: number) => ({
|
||||||
...signer,
|
...signer,
|
||||||
signingOrder: index + 1,
|
signingOrder: signingOrder === DocumentSigningOrder.SEQUENTIAL ? index + 1 : undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update the form with new ordering
|
// Update the form with new ordering
|
||||||
@ -132,6 +135,16 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
[move, replace, getValues],
|
[move, replace, getValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSigningOrderChange = (signingOrder: DocumentSigningOrder) => {
|
||||||
|
setValue('meta.signingOrder', signingOrder);
|
||||||
|
|
||||||
|
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
|
signers.forEach((_signer, index) => {
|
||||||
|
setValue(`signers.${index}.signingOrder`, index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-foreground mb-1 text-lg font-medium">
|
<h3 className="text-foreground mb-1 text-lg font-medium">
|
||||||
@ -152,11 +165,11 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
{...field}
|
{...field}
|
||||||
id="signingOrder"
|
id="signingOrder"
|
||||||
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) =>
|
||||||
field.onChange(
|
onSigningOrderChange(
|
||||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||||
);
|
)
|
||||||
}}
|
}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -184,6 +197,7 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
disabled={isSubmitting || !isSigningOrderEnabled}
|
disabled={isSubmitting || !isSigningOrderEnabled}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FormLabel
|
<FormLabel
|
||||||
htmlFor="allowDictateNextSigner"
|
htmlFor="allowDictateNextSigner"
|
||||||
@ -227,13 +241,14 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
key={signer.id}
|
key={signer.id}
|
||||||
draggableId={signer.id}
|
draggableId={signer.id}
|
||||||
index={index}
|
index={index}
|
||||||
isDragDisabled={!isSigningOrderEnabled || isSubmitting}
|
isDragDisabled={!isSigningOrderEnabled || isSubmitting || signer.disabled}
|
||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<fieldset
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
|
disabled={signer.disabled}
|
||||||
className={cn('py-1', {
|
className={cn('py-1', {
|
||||||
'bg-widget-foreground pointer-events-none rounded-md pt-2':
|
'bg-widget-foreground pointer-events-none rounded-md pt-2':
|
||||||
snapshot.isDragging,
|
snapshot.isDragging,
|
||||||
@ -349,7 +364,7 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
{...field}
|
{...field}
|
||||||
isAssistantEnabled={isSigningOrderEnabled}
|
isAssistantEnabled={isSigningOrderEnabled}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
disabled={isSubmitting || snapshot.isDragging}
|
disabled={isSubmitting || snapshot.isDragging || signer.disabled}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -360,13 +375,18 @@ export const ConfigureDocumentRecipients = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
disabled={isSubmitting || signers.length === 1 || snapshot.isDragging}
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
signers.length === 1 ||
|
||||||
|
snapshot.isDragging ||
|
||||||
|
signer.disabled
|
||||||
|
}
|
||||||
onClick={() => removeSigner(index)}
|
onClick={() => removeSigner(index)}
|
||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -30,10 +30,15 @@ import {
|
|||||||
export interface ConfigureDocumentViewProps {
|
export interface ConfigureDocumentViewProps {
|
||||||
onSubmit: (data: TConfigureEmbedFormSchema) => void | Promise<void>;
|
onSubmit: (data: TConfigureEmbedFormSchema) => void | Promise<void>;
|
||||||
defaultValues?: Partial<TConfigureEmbedFormSchema>;
|
defaultValues?: Partial<TConfigureEmbedFormSchema>;
|
||||||
|
disableUpload?: boolean;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigureDocumentView = ({ onSubmit, defaultValues }: ConfigureDocumentViewProps) => {
|
export const ConfigureDocumentView = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
disableUpload,
|
||||||
|
}: ConfigureDocumentViewProps) => {
|
||||||
const { isTemplate } = useConfigureDocument();
|
const { isTemplate } = useConfigureDocument();
|
||||||
|
|
||||||
const form = useForm<TConfigureEmbedFormSchema>({
|
const form = useForm<TConfigureEmbedFormSchema>({
|
||||||
@ -47,6 +52,7 @@ export const ConfigureDocumentView = ({ onSubmit, defaultValues }: ConfigureDocu
|
|||||||
email: isTemplate ? `recipient.${1}@document.com` : '',
|
email: isTemplate ? `recipient.${1}@document.com` : '',
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
signingOrder: 1,
|
signingOrder: 1,
|
||||||
|
disabled: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
meta: {
|
meta: {
|
||||||
@ -110,7 +116,7 @@ export const ConfigureDocumentView = ({ onSubmit, defaultValues }: ConfigureDocu
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfigureDocumentUpload isSubmitting={isSubmitting} />
|
{!disableUpload && <ConfigureDocumentUpload isSubmitting={isSubmitting} />}
|
||||||
<ConfigureDocumentRecipients control={control} isSubmitting={isSubmitting} />
|
<ConfigureDocumentRecipients control={control} isSubmitting={isSubmitting} />
|
||||||
<ConfigureDocumentAdvancedSettings control={control} isSubmitting={isSubmitting} />
|
<ConfigureDocumentAdvancedSettings control={control} isSubmitting={isSubmitting} />
|
||||||
|
|
||||||
|
|||||||
@ -15,11 +15,13 @@ export const ZConfigureEmbedFormSchema = z.object({
|
|||||||
signers: z
|
signers: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
nativeId: z.number().optional(),
|
||||||
formId: z.string(),
|
formId: z.string(),
|
||||||
name: z.string().min(1, { message: 'Name is required' }),
|
name: z.string().min(1, { message: 'Name is required' }),
|
||||||
email: z.string().email('Invalid email address'),
|
email: z.string().email('Invalid email address'),
|
||||||
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.min(1, { message: 'At least one signer is required' }),
|
.min(1, { message: 'At least one signer is required' }),
|
||||||
@ -34,7 +36,7 @@ export const ZConfigureEmbedFormSchema = z.object({
|
|||||||
language: ZDocumentMetaLanguageSchema.optional(),
|
language: ZDocumentMetaLanguageSchema.optional(),
|
||||||
signatureTypes: z.array(z.string()).default([]),
|
signatureTypes: z.array(z.string()).default([]),
|
||||||
signingOrder: z.enum(['SEQUENTIAL', 'PARALLEL']),
|
signingOrder: z.enum(['SEQUENTIAL', 'PARALLEL']),
|
||||||
allowDictateNextSigner: z.boolean().default(false),
|
allowDictateNextSigner: z.boolean().default(false).optional(),
|
||||||
externalId: z.string().optional(),
|
externalId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
documentData: z
|
documentData: z
|
||||||
|
|||||||
@ -2,12 +2,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { DocumentData } from '@prisma/client';
|
import type { DocumentData, FieldType } from '@prisma/client';
|
||||||
import { FieldType, ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
||||||
import { ChevronsUpDown } from 'lucide-react';
|
import { ChevronsUpDown } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||||
@ -30,6 +29,7 @@ import { Sheet, SheetContent, SheetTrigger } from '@documenso/ui/primitives/shee
|
|||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import type { TConfigureEmbedFormSchema } from './configure-document-view.types';
|
import type { TConfigureEmbedFormSchema } from './configure-document-view.types';
|
||||||
|
import type { TConfigureFieldsFormSchema } from './configure-fields-view.types';
|
||||||
import { FieldAdvancedSettingsDrawer } from './field-advanced-settings-drawer';
|
import { FieldAdvancedSettingsDrawer } from './field-advanced-settings-drawer';
|
||||||
|
|
||||||
const MIN_HEIGHT_PX = 12;
|
const MIN_HEIGHT_PX = 12;
|
||||||
@ -38,28 +38,9 @@ const MIN_WIDTH_PX = 36;
|
|||||||
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
|
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
|
||||||
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
|
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
|
||||||
|
|
||||||
export const ZConfigureFieldsFormSchema = z.object({
|
|
||||||
fields: z.array(
|
|
||||||
z.object({
|
|
||||||
formId: z.string().min(1),
|
|
||||||
id: z.string().min(1),
|
|
||||||
type: z.nativeEnum(FieldType),
|
|
||||||
signerEmail: z.string().min(1),
|
|
||||||
recipientId: z.number().min(0),
|
|
||||||
pageNumber: z.number().min(1),
|
|
||||||
pageX: z.number().min(0),
|
|
||||||
pageY: z.number().min(0),
|
|
||||||
pageWidth: z.number().min(0),
|
|
||||||
pageHeight: z.number().min(0),
|
|
||||||
fieldMeta: ZFieldMetaSchema.optional(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TConfigureFieldsFormSchema = z.infer<typeof ZConfigureFieldsFormSchema>;
|
|
||||||
|
|
||||||
export type ConfigureFieldsViewProps = {
|
export type ConfigureFieldsViewProps = {
|
||||||
configData: TConfigureEmbedFormSchema;
|
configData: TConfigureEmbedFormSchema;
|
||||||
|
documentData?: DocumentData;
|
||||||
defaultValues?: Partial<TConfigureFieldsFormSchema>;
|
defaultValues?: Partial<TConfigureFieldsFormSchema>;
|
||||||
onBack: (data: TConfigureFieldsFormSchema) => void;
|
onBack: (data: TConfigureFieldsFormSchema) => void;
|
||||||
onSubmit: (data: TConfigureFieldsFormSchema) => void;
|
onSubmit: (data: TConfigureFieldsFormSchema) => void;
|
||||||
@ -67,13 +48,14 @@ export type ConfigureFieldsViewProps = {
|
|||||||
|
|
||||||
export const ConfigureFieldsView = ({
|
export const ConfigureFieldsView = ({
|
||||||
configData,
|
configData,
|
||||||
|
documentData,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
onBack,
|
onBack,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: ConfigureFieldsViewProps) => {
|
}: ConfigureFieldsViewProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
// Track if we're on a mobile device
|
// Track if we're on a mobile device
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
@ -99,7 +81,11 @@ export const ConfigureFieldsView = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const documentData = useMemo(() => {
|
const normalizedDocumentData = useMemo(() => {
|
||||||
|
if (documentData) {
|
||||||
|
return documentData;
|
||||||
|
}
|
||||||
|
|
||||||
if (!configData.documentData) {
|
if (!configData.documentData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -116,7 +102,7 @@ export const ConfigureFieldsView = ({
|
|||||||
|
|
||||||
const recipients = useMemo(() => {
|
const recipients = useMemo(() => {
|
||||||
return configData.signers.map<Recipient>((signer, index) => ({
|
return configData.signers.map<Recipient>((signer, index) => ({
|
||||||
id: index,
|
id: signer.nativeId || index,
|
||||||
name: signer.name || '',
|
name: signer.name || '',
|
||||||
email: signer.email || '',
|
email: signer.email || '',
|
||||||
role: signer.role,
|
role: signer.role,
|
||||||
@ -129,14 +115,14 @@ export const ConfigureFieldsView = ({
|
|||||||
signedAt: null,
|
signedAt: null,
|
||||||
authOptions: null,
|
authOptions: null,
|
||||||
rejectionReason: null,
|
rejectionReason: null,
|
||||||
sendStatus: SendStatus.NOT_SENT,
|
sendStatus: signer.disabled ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||||
readStatus: ReadStatus.NOT_OPENED,
|
readStatus: signer.disabled ? ReadStatus.OPENED : ReadStatus.NOT_OPENED,
|
||||||
signingStatus: SigningStatus.NOT_SIGNED,
|
signingStatus: signer.disabled ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||||
}));
|
}));
|
||||||
}, [configData.signers]);
|
}, [configData.signers]);
|
||||||
|
|
||||||
const [selectedRecipient, setSelectedRecipient] = useState<Recipient | null>(
|
const [selectedRecipient, setSelectedRecipient] = useState<Recipient | null>(
|
||||||
() => recipients[0] || null,
|
() => recipients.find((r) => r.signingStatus === SigningStatus.NOT_SIGNED) || null,
|
||||||
);
|
);
|
||||||
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
||||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||||
@ -206,8 +192,8 @@ export const ConfigureFieldsView = ({
|
|||||||
|
|
||||||
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
||||||
...structuredClone(lastActiveField),
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
id: nanoid(12),
|
|
||||||
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
||||||
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
||||||
pageX: lastActiveField.pageX + 3,
|
pageX: lastActiveField.pageX + 3,
|
||||||
@ -229,8 +215,8 @@ export const ConfigureFieldsView = ({
|
|||||||
|
|
||||||
append({
|
append({
|
||||||
...copiedField,
|
...copiedField,
|
||||||
|
nativeId: undefined,
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
id: nanoid(12),
|
|
||||||
signerEmail: selectedRecipient?.email ?? copiedField.signerEmail,
|
signerEmail: selectedRecipient?.email ?? copiedField.signerEmail,
|
||||||
recipientId: selectedRecipient?.id ?? copiedField.recipientId,
|
recipientId: selectedRecipient?.id ?? copiedField.recipientId,
|
||||||
pageX: copiedField.pageX + 3,
|
pageX: copiedField.pageX + 3,
|
||||||
@ -303,7 +289,6 @@ export const ConfigureFieldsView = ({
|
|||||||
pageY -= fieldPageHeight / 2;
|
pageY -= fieldPageHeight / 2;
|
||||||
|
|
||||||
const field = {
|
const field = {
|
||||||
id: nanoid(12),
|
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
type: selectedField,
|
type: selectedField,
|
||||||
pageNumber,
|
pageNumber,
|
||||||
@ -526,9 +511,9 @@ export const ConfigureFieldsView = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
{documentData && (
|
{normalizedDocumentData && (
|
||||||
<div>
|
<div>
|
||||||
<PDFViewer documentData={documentData} />
|
<PDFViewer documentData={normalizedDocumentData} />
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
{localFields.map((field, index) => {
|
{localFields.map((field, index) => {
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export const ZConfigureFieldsFormSchema = z.object({
|
||||||
|
fields: z.array(
|
||||||
|
z.object({
|
||||||
|
nativeId: z.number().optional(),
|
||||||
|
formId: z.string().min(1),
|
||||||
|
type: z.nativeEnum(FieldType),
|
||||||
|
signerEmail: z.string().min(1),
|
||||||
|
inserted: z.boolean().optional(),
|
||||||
|
recipientId: z.number().min(0),
|
||||||
|
pageNumber: z.number().min(1),
|
||||||
|
pageX: z.number().min(0),
|
||||||
|
pageY: z.number().min(0),
|
||||||
|
pageWidth: z.number().min(0),
|
||||||
|
pageHeight: z.number().min(0),
|
||||||
|
fieldMeta: ZFieldMetaSchema.optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TConfigureFieldsFormSchema = z.infer<typeof ZConfigureFieldsFormSchema>;
|
||||||
|
|
||||||
|
export type TConfigureFieldsFormSchemaField = z.infer<
|
||||||
|
typeof ZConfigureFieldsFormSchema
|
||||||
|
>['fields'][number];
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import type { FieldType } from '@prisma/client';
|
|
||||||
|
|
||||||
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
|
import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
|
||||||
@ -8,35 +7,13 @@ import { FieldAdvancedSettings } from '@documenso/ui/primitives/document-flow/fi
|
|||||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
||||||
import { Sheet, SheetContent, SheetTitle } from '@documenso/ui/primitives/sheet';
|
import { Sheet, SheetContent, SheetTitle } from '@documenso/ui/primitives/sheet';
|
||||||
|
|
||||||
|
import type { TConfigureFieldsFormSchemaField } from './configure-fields-view.types';
|
||||||
|
|
||||||
export type FieldAdvancedSettingsDrawerProps = {
|
export type FieldAdvancedSettingsDrawerProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
currentField: {
|
currentField: TConfigureFieldsFormSchemaField | null;
|
||||||
id: string;
|
fields: TConfigureFieldsFormSchemaField[];
|
||||||
formId: string;
|
|
||||||
type: FieldType;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
pageWidth: number;
|
|
||||||
pageHeight: number;
|
|
||||||
recipientId: number;
|
|
||||||
signerEmail: string;
|
|
||||||
fieldMeta?: FieldMeta;
|
|
||||||
} | null;
|
|
||||||
fields: Array<{
|
|
||||||
id: string;
|
|
||||||
formId: string;
|
|
||||||
type: FieldType;
|
|
||||||
pageNumber: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
pageWidth: number;
|
|
||||||
pageHeight: number;
|
|
||||||
recipientId: number;
|
|
||||||
signerEmail: string;
|
|
||||||
fieldMeta?: FieldMeta;
|
|
||||||
}>;
|
|
||||||
onFieldUpdate: (formId: string, fieldMeta: FieldMeta) => void;
|
onFieldUpdate: (formId: string, fieldMeta: FieldMeta) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
102
apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
Normal file
102
apps/remix/app/routes/embed+/v1+/authoring+/_layout.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { Outlet, useLoaderData } from 'react-router';
|
||||||
|
|
||||||
|
import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
|
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
|
import { TrpcProvider } from '@documenso/trpc/react';
|
||||||
|
|
||||||
|
import { ZBaseEmbedAuthoringSchema } from '~/types/embed-authoring-base-schema';
|
||||||
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
|
import type { Route } from './+types/_layout';
|
||||||
|
|
||||||
|
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
hasValidToken: false,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
||||||
|
|
||||||
|
let hasPlatformPlan = false;
|
||||||
|
let hasEnterprisePlan = false;
|
||||||
|
let hasCommunityPlan = false;
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
[hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([
|
||||||
|
isCommunityPlan({
|
||||||
|
userId: result.userId,
|
||||||
|
teamId: result.teamId ?? undefined,
|
||||||
|
}),
|
||||||
|
isDocumentPlatform({
|
||||||
|
userId: result.userId,
|
||||||
|
teamId: result.teamId,
|
||||||
|
}),
|
||||||
|
isUserEnterprise({
|
||||||
|
userId: result.userId,
|
||||||
|
teamId: result.teamId ?? undefined,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasValidToken: !!result,
|
||||||
|
token,
|
||||||
|
hasCommunityPlan,
|
||||||
|
hasPlatformPlan,
|
||||||
|
hasEnterprisePlan,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AuthoringLayout() {
|
||||||
|
const { hasValidToken, token, hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan } =
|
||||||
|
useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
try {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||||
|
JSON.parse(decodeURIComponent(atob(hash))),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { css, cssVars, darkModeDisabled } = result.data;
|
||||||
|
|
||||||
|
if (darkModeDisabled) {
|
||||||
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCommunityPlan || hasPlatformPlan || hasEnterprisePlan) {
|
||||||
|
injectCss({
|
||||||
|
css,
|
||||||
|
cssVars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasValidToken) {
|
||||||
|
return <div>Invalid embedding presign token provided</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrpcProvider headers={{ authorization: `Bearer ${token}` }}>
|
||||||
|
<Outlet />
|
||||||
|
</TrpcProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -12,10 +12,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
||||||
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
||||||
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
||||||
import {
|
import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view';
|
||||||
ConfigureFieldsView,
|
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
|
||||||
type TConfigureFieldsFormSchema,
|
|
||||||
} from '~/components/embed/authoring/configure-fields-view';
|
|
||||||
import {
|
import {
|
||||||
type TBaseEmbedAuthoringSchema,
|
type TBaseEmbedAuthoringSchema,
|
||||||
ZBaseEmbedAuthoringSchema,
|
ZBaseEmbedAuthoringSchema,
|
||||||
@ -71,6 +69,8 @@ export default function EmbeddingAuthoringDocumentCreatePage() {
|
|||||||
// Use the externalId from the URL fragment if available
|
// Use the externalId from the URL fragment if available
|
||||||
const documentExternalId = externalId || configuration.meta.externalId;
|
const documentExternalId = externalId || configuration.meta.externalId;
|
||||||
|
|
||||||
|
const signatureTypes = configuration.meta.signatureTypes ?? [];
|
||||||
|
|
||||||
const createResult = await createEmbeddingDocument({
|
const createResult = await createEmbeddingDocument({
|
||||||
title: configuration.title,
|
title: configuration.title,
|
||||||
documentDataId: documentData.id,
|
documentDataId: documentData.id,
|
||||||
@ -78,14 +78,11 @@ export default function EmbeddingAuthoringDocumentCreatePage() {
|
|||||||
meta: {
|
meta: {
|
||||||
...configuration.meta,
|
...configuration.meta,
|
||||||
drawSignatureEnabled:
|
drawSignatureEnabled:
|
||||||
configuration.meta.signatureTypes.length === 0 ||
|
signatureTypes.length === 0 || signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||||
configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW),
|
|
||||||
typedSignatureEnabled:
|
typedSignatureEnabled:
|
||||||
configuration.meta.signatureTypes.length === 0 ||
|
signatureTypes.length === 0 || signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||||
configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE),
|
|
||||||
uploadSignatureEnabled:
|
uploadSignatureEnabled:
|
||||||
configuration.meta.signatureTypes.length === 0 ||
|
signatureTypes.length === 0 || signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||||
configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
|
||||||
},
|
},
|
||||||
recipients: configuration.signers.map((signer) => ({
|
recipients: configuration.signers.map((signer) => ({
|
||||||
name: signer.name,
|
name: signer.name,
|
||||||
@ -126,7 +123,7 @@ export default function EmbeddingAuthoringDocumentCreatePage() {
|
|||||||
|
|
||||||
// Navigate to the completion page instead of the document details page
|
// Navigate to the completion page instead of the document details page
|
||||||
await navigate(
|
await navigate(
|
||||||
`/embed/v1/authoring/create-completed?documentId=${createResult.documentId}&externalId=${documentExternalId}#${hash}`,
|
`/embed/v1/authoring/completed/create?documentId=${createResult.documentId}&externalId=${documentExternalId}#${hash}`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating document:', err);
|
console.error('Error creating document:', err);
|
||||||
@ -0,0 +1,314 @@
|
|||||||
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { DocumentDistributionMethod, DocumentSigningOrder, SigningStatus } from '@prisma/client';
|
||||||
|
import { redirect, useLoaderData } from 'react-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
isValidDateFormat,
|
||||||
|
} from '@documenso/lib/constants/date-formats';
|
||||||
|
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
|
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||||
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
|
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||||
|
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
||||||
|
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
||||||
|
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
||||||
|
import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view';
|
||||||
|
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
|
||||||
|
import {
|
||||||
|
type TBaseEmbedAuthoringSchema,
|
||||||
|
ZBaseEmbedAuthoringSchema,
|
||||||
|
} from '~/types/embed-authoring-base-schema';
|
||||||
|
|
||||||
|
import type { Route } from './+types/document.edit.$id';
|
||||||
|
|
||||||
|
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// We know that the token is present because we're checking it in the parent _layout route
|
||||||
|
const token = url.searchParams.get('token') || '';
|
||||||
|
|
||||||
|
// We also know that the token is valid, but we need the userId + teamId
|
||||||
|
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentId = Number(id);
|
||||||
|
|
||||||
|
if (!documentId || Number.isNaN(documentId)) {
|
||||||
|
redirect(`/embed/v1/authoring/error/not-found?documentId=${documentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = await getDocumentWithDetailsById({
|
||||||
|
documentId,
|
||||||
|
userId: result?.userId,
|
||||||
|
teamId: result?.teamId ?? undefined,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw redirect(`/embed/v1/authoring/error/not-found?documentId=${documentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = document.fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
positionX: field.positionX.toNumber(),
|
||||||
|
positionY: field.positionY.toNumber(),
|
||||||
|
width: field.width.toNumber(),
|
||||||
|
height: field.height.toNumber(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: {
|
||||||
|
...document,
|
||||||
|
fields,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EmbeddingAuthoringDocumentEditPage() {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { document } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
const signatureTypes = useMemo(() => {
|
||||||
|
const types: string[] = [];
|
||||||
|
|
||||||
|
if (document.documentMeta?.drawSignatureEnabled) {
|
||||||
|
types.push(DocumentSignatureType.DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.documentMeta?.typedSignatureEnabled) {
|
||||||
|
types.push(DocumentSignatureType.TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.documentMeta?.uploadSignatureEnabled) {
|
||||||
|
types.push(DocumentSignatureType.UPLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}, [document.documentMeta]);
|
||||||
|
|
||||||
|
const [configuration, setConfiguration] = useState<TConfigureEmbedFormSchema | null>(() => ({
|
||||||
|
title: document.title,
|
||||||
|
documentData: undefined,
|
||||||
|
meta: {
|
||||||
|
subject: document.documentMeta?.subject ?? undefined,
|
||||||
|
message: document.documentMeta?.message ?? undefined,
|
||||||
|
distributionMethod:
|
||||||
|
document.documentMeta?.distributionMethod ?? DocumentDistributionMethod.EMAIL,
|
||||||
|
emailSettings: document.documentMeta?.emailSettings ?? ZDocumentEmailSettingsSchema.parse({}),
|
||||||
|
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
|
signingOrder: document.documentMeta?.signingOrder ?? DocumentSigningOrder.PARALLEL,
|
||||||
|
allowDictateNextSigner: document.documentMeta?.allowDictateNextSigner ?? false,
|
||||||
|
language: isValidLanguageCode(document.documentMeta?.language)
|
||||||
|
? document.documentMeta.language
|
||||||
|
: undefined,
|
||||||
|
signatureTypes: signatureTypes,
|
||||||
|
dateFormat: isValidDateFormat(document.documentMeta?.dateFormat)
|
||||||
|
? document.documentMeta?.dateFormat
|
||||||
|
: DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
redirectUrl: document.documentMeta?.redirectUrl ?? undefined,
|
||||||
|
},
|
||||||
|
signers: document.recipients.map((recipient) => ({
|
||||||
|
nativeId: recipient.id,
|
||||||
|
formId: nanoid(8),
|
||||||
|
name: recipient.name,
|
||||||
|
email: recipient.email,
|
||||||
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder ?? undefined,
|
||||||
|
disabled: recipient.signingStatus !== SigningStatus.NOT_SIGNED,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [fields, setFields] = useState<TConfigureFieldsFormSchema | null>(() => ({
|
||||||
|
fields: document.fields.map((field) => ({
|
||||||
|
nativeId: field.id,
|
||||||
|
formId: nanoid(8),
|
||||||
|
type: field.type,
|
||||||
|
signerEmail:
|
||||||
|
document.recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||||
|
inserted: field.inserted,
|
||||||
|
recipientId: field.recipientId,
|
||||||
|
pageNumber: field.page,
|
||||||
|
pageX: field.positionX,
|
||||||
|
pageY: field.positionY,
|
||||||
|
pageWidth: field.width,
|
||||||
|
pageHeight: field.height,
|
||||||
|
fieldMeta: field.fieldMeta ?? undefined,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
|
||||||
|
const [externalId, setExternalId] = useState<string | null>(null);
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
|
||||||
|
const { mutateAsync: updateEmbeddingDocument } =
|
||||||
|
trpc.embeddingPresign.updateEmbeddingDocument.useMutation();
|
||||||
|
|
||||||
|
const handleConfigurePageViewSubmit = (data: TConfigureEmbedFormSchema) => {
|
||||||
|
// Store the configuration data and move to the field placement stage
|
||||||
|
setConfiguration(data);
|
||||||
|
setFields((fieldData) => {
|
||||||
|
if (!fieldData) {
|
||||||
|
return fieldData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signerEmails = data.signers.map((signer) => signer.email);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setCurrentStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToConfig = (data: TConfigureFieldsFormSchema) => {
|
||||||
|
// Return to the configuration view but keep the data
|
||||||
|
setFields(data);
|
||||||
|
setCurrentStep(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => {
|
||||||
|
try {
|
||||||
|
if (!configuration) {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: _('Error'),
|
||||||
|
description: _('Please configure the document first'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = data.fields;
|
||||||
|
|
||||||
|
// Use the externalId from the URL fragment if available
|
||||||
|
const documentExternalId = externalId || configuration.meta.externalId;
|
||||||
|
|
||||||
|
const updateResult = await updateEmbeddingDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
title: configuration.title,
|
||||||
|
externalId: documentExternalId,
|
||||||
|
meta: {
|
||||||
|
...configuration.meta,
|
||||||
|
drawSignatureEnabled: configuration.meta.signatureTypes
|
||||||
|
? configuration.meta.signatureTypes.length === 0 ||
|
||||||
|
configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW)
|
||||||
|
: undefined,
|
||||||
|
typedSignatureEnabled: configuration.meta.signatureTypes
|
||||||
|
? configuration.meta.signatureTypes.length === 0 ||
|
||||||
|
configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE)
|
||||||
|
: undefined,
|
||||||
|
uploadSignatureEnabled: configuration.meta.signatureTypes
|
||||||
|
? configuration.meta.signatureTypes.length === 0 ||
|
||||||
|
configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
recipients: configuration.signers.map((signer) => ({
|
||||||
|
id: signer.nativeId,
|
||||||
|
name: signer.name,
|
||||||
|
email: signer.email,
|
||||||
|
role: signer.role,
|
||||||
|
signingOrder: signer.signingOrder,
|
||||||
|
fields: fields
|
||||||
|
.filter((field) => field.signerEmail === signer.email)
|
||||||
|
// 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,
|
||||||
|
pageX: f.pageX,
|
||||||
|
pageY: f.pageY,
|
||||||
|
width: f.pageWidth,
|
||||||
|
height: f.pageHeight,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _('Success'),
|
||||||
|
description: _('Document updated successfully'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a message to the parent window with the document details
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'document-updated',
|
||||||
|
// documentId: updateResult.documentId,
|
||||||
|
documentId: 1,
|
||||||
|
externalId: documentExternalId,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating document:', err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: _('Error'),
|
||||||
|
description: _('Failed to update document'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
try {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||||
|
JSON.parse(decodeURIComponent(atob(hash))),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeatures(result.data.features);
|
||||||
|
|
||||||
|
// Extract externalId from the parsed data if available
|
||||||
|
if (result.data.externalId) {
|
||||||
|
setExternalId(result.data.externalId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing embedding params:', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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
|
||||||
|
defaultValues={configuration ?? undefined}
|
||||||
|
disableUpload={true}
|
||||||
|
onSubmit={handleConfigurePageViewSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfigureFieldsView
|
||||||
|
configData={configuration!}
|
||||||
|
documentData={document.documentData}
|
||||||
|
defaultValues={fields ?? undefined}
|
||||||
|
onBack={handleBackToConfig}
|
||||||
|
onSubmit={handleConfigureFieldsSubmit}
|
||||||
|
/>
|
||||||
|
</Stepper>
|
||||||
|
</ConfigureDocumentProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -11,10 +11,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
||||||
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
||||||
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
||||||
import {
|
import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view';
|
||||||
ConfigureFieldsView,
|
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
|
||||||
type TConfigureFieldsFormSchema,
|
|
||||||
} from '~/components/embed/authoring/configure-fields-view';
|
|
||||||
import {
|
import {
|
||||||
type TBaseEmbedAuthoringSchema,
|
type TBaseEmbedAuthoringSchema,
|
||||||
ZBaseEmbedAuthoringSchema,
|
ZBaseEmbedAuthoringSchema,
|
||||||
@ -48,8 +46,6 @@ export default function EmbeddingAuthoringTemplateCreatePage() {
|
|||||||
|
|
||||||
const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => {
|
const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
console.log('configuration', configuration);
|
|
||||||
console.log('data', data);
|
|
||||||
if (!configuration || !configuration.documentData) {
|
if (!configuration || !configuration.documentData) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
@ -117,7 +113,7 @@ export default function EmbeddingAuthoringTemplateCreatePage() {
|
|||||||
|
|
||||||
// Navigate to the completion page instead of the template details page
|
// Navigate to the completion page instead of the template details page
|
||||||
await navigate(
|
await navigate(
|
||||||
`/embed/v1/authoring/create-completed?templateId=${createResult.templateId}&externalId=${metaWithExternalId.externalId}#${hash}`,
|
`/embed/v1/authoring/completed/create?templateId=${createResult.templateId}&externalId=${metaWithExternalId.externalId}#${hash}`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating template:', err);
|
console.error('Error creating template:', err);
|
||||||
@ -0,0 +1,314 @@
|
|||||||
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { DocumentDistributionMethod, DocumentSigningOrder, SigningStatus } from '@prisma/client';
|
||||||
|
import { redirect, useLoaderData } from 'react-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
isValidDateFormat,
|
||||||
|
} from '@documenso/lib/constants/date-formats';
|
||||||
|
import { DocumentSignatureType } from '@documenso/lib/constants/document';
|
||||||
|
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||||
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
|
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { ConfigureDocumentProvider } from '~/components/embed/authoring/configure-document-context';
|
||||||
|
import { ConfigureDocumentView } from '~/components/embed/authoring/configure-document-view';
|
||||||
|
import type { TConfigureEmbedFormSchema } from '~/components/embed/authoring/configure-document-view.types';
|
||||||
|
import { ConfigureFieldsView } from '~/components/embed/authoring/configure-fields-view';
|
||||||
|
import type { TConfigureFieldsFormSchema } from '~/components/embed/authoring/configure-fields-view.types';
|
||||||
|
import {
|
||||||
|
type TBaseEmbedAuthoringSchema,
|
||||||
|
ZBaseEmbedAuthoringSchema,
|
||||||
|
} from '~/types/embed-authoring-base-schema';
|
||||||
|
|
||||||
|
import type { Route } from './+types/document.edit.$id';
|
||||||
|
|
||||||
|
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// We know that the token is present because we're checking it in the parent _layout route
|
||||||
|
const token = url.searchParams.get('token') || '';
|
||||||
|
|
||||||
|
// We also know that the token is valid, but we need the userId + teamId
|
||||||
|
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateId = Number(id);
|
||||||
|
|
||||||
|
if (!templateId || Number.isNaN(templateId)) {
|
||||||
|
redirect(`/embed/v1/authoring/error/not-found?templateId=${templateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = await getTemplateById({
|
||||||
|
id: templateId,
|
||||||
|
userId: result?.userId,
|
||||||
|
teamId: result?.teamId ?? undefined,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
throw redirect(`/embed/v1/authoring/error/not-found?templateId=${templateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = template.fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
positionX: field.positionX.toNumber(),
|
||||||
|
positionY: field.positionY.toNumber(),
|
||||||
|
width: field.width.toNumber(),
|
||||||
|
height: field.height.toNumber(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
template: {
|
||||||
|
...template,
|
||||||
|
fields,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EmbeddingAuthoringTemplateEditPage() {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { template } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
const signatureTypes = useMemo(() => {
|
||||||
|
const types: string[] = [];
|
||||||
|
|
||||||
|
if (template.templateMeta?.drawSignatureEnabled) {
|
||||||
|
types.push(DocumentSignatureType.DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.templateMeta?.typedSignatureEnabled) {
|
||||||
|
types.push(DocumentSignatureType.TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.templateMeta?.uploadSignatureEnabled) {
|
||||||
|
types.push(DocumentSignatureType.UPLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}, [template.templateMeta]);
|
||||||
|
|
||||||
|
const [configuration, setConfiguration] = useState<TConfigureEmbedFormSchema | null>(() => ({
|
||||||
|
title: template.title,
|
||||||
|
documentData: undefined,
|
||||||
|
meta: {
|
||||||
|
subject: template.templateMeta?.subject ?? undefined,
|
||||||
|
message: template.templateMeta?.message ?? undefined,
|
||||||
|
distributionMethod:
|
||||||
|
template.templateMeta?.distributionMethod ?? DocumentDistributionMethod.EMAIL,
|
||||||
|
emailSettings: template.templateMeta?.emailSettings ?? ZDocumentEmailSettingsSchema.parse({}),
|
||||||
|
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
|
signingOrder: template.templateMeta?.signingOrder ?? DocumentSigningOrder.PARALLEL,
|
||||||
|
allowDictateNextSigner: template.templateMeta?.allowDictateNextSigner ?? false,
|
||||||
|
language: isValidLanguageCode(template.templateMeta?.language)
|
||||||
|
? template.templateMeta.language
|
||||||
|
: undefined,
|
||||||
|
signatureTypes: signatureTypes,
|
||||||
|
dateFormat: isValidDateFormat(template.templateMeta?.dateFormat)
|
||||||
|
? template.templateMeta?.dateFormat
|
||||||
|
: DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
redirectUrl: template.templateMeta?.redirectUrl ?? undefined,
|
||||||
|
},
|
||||||
|
signers: template.recipients.map((recipient) => ({
|
||||||
|
nativeId: recipient.id,
|
||||||
|
formId: nanoid(8),
|
||||||
|
name: recipient.name,
|
||||||
|
email: recipient.email,
|
||||||
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder ?? undefined,
|
||||||
|
disabled: recipient.signingStatus !== SigningStatus.NOT_SIGNED,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [fields, setFields] = useState<TConfigureFieldsFormSchema | null>(() => ({
|
||||||
|
fields: template.fields.map((field) => ({
|
||||||
|
nativeId: field.id,
|
||||||
|
formId: nanoid(8),
|
||||||
|
type: field.type,
|
||||||
|
signerEmail:
|
||||||
|
template.recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||||
|
inserted: field.inserted,
|
||||||
|
recipientId: field.recipientId,
|
||||||
|
pageNumber: field.page,
|
||||||
|
pageX: field.positionX,
|
||||||
|
pageY: field.positionY,
|
||||||
|
pageWidth: field.width,
|
||||||
|
pageHeight: field.height,
|
||||||
|
fieldMeta: field.fieldMeta ?? undefined,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [features, setFeatures] = useState<TBaseEmbedAuthoringSchema['features'] | null>(null);
|
||||||
|
const [externalId, setExternalId] = useState<string | null>(null);
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
|
||||||
|
const { mutateAsync: updateEmbeddingTemplate } =
|
||||||
|
trpc.embeddingPresign.updateEmbeddingTemplate.useMutation();
|
||||||
|
|
||||||
|
const handleConfigurePageViewSubmit = (data: TConfigureEmbedFormSchema) => {
|
||||||
|
// Store the configuration data and move to the field placement stage
|
||||||
|
setConfiguration(data);
|
||||||
|
setFields((fieldData) => {
|
||||||
|
if (!fieldData) {
|
||||||
|
return fieldData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signerEmails = data.signers.map((signer) => signer.email);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: fieldData.fields.filter((field) => signerEmails.includes(field.signerEmail)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setCurrentStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToConfig = (data: TConfigureFieldsFormSchema) => {
|
||||||
|
// Return to the configuration view but keep the data
|
||||||
|
setFields(data);
|
||||||
|
setCurrentStep(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigureFieldsSubmit = async (data: TConfigureFieldsFormSchema) => {
|
||||||
|
try {
|
||||||
|
if (!configuration) {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: _('Error'),
|
||||||
|
description: _('Please configure the document first'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = data.fields;
|
||||||
|
|
||||||
|
// Use the externalId from the URL fragment if available
|
||||||
|
const templateExternalId = externalId || configuration.meta.externalId;
|
||||||
|
|
||||||
|
const updateResult = await updateEmbeddingTemplate({
|
||||||
|
templateId: template.id,
|
||||||
|
title: configuration.title,
|
||||||
|
externalId: templateExternalId,
|
||||||
|
meta: {
|
||||||
|
...configuration.meta,
|
||||||
|
drawSignatureEnabled: configuration.meta.signatureTypes
|
||||||
|
? configuration.meta.signatureTypes.length === 0 ||
|
||||||
|
configuration.meta.signatureTypes.includes(DocumentSignatureType.DRAW)
|
||||||
|
: undefined,
|
||||||
|
typedSignatureEnabled: configuration.meta.signatureTypes
|
||||||
|
? configuration.meta.signatureTypes.length === 0 ||
|
||||||
|
configuration.meta.signatureTypes.includes(DocumentSignatureType.TYPE)
|
||||||
|
: undefined,
|
||||||
|
uploadSignatureEnabled: configuration.meta.signatureTypes
|
||||||
|
? configuration.meta.signatureTypes.length === 0 ||
|
||||||
|
configuration.meta.signatureTypes.includes(DocumentSignatureType.UPLOAD)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
recipients: configuration.signers.map((signer) => ({
|
||||||
|
id: signer.nativeId,
|
||||||
|
name: signer.name,
|
||||||
|
email: signer.email,
|
||||||
|
role: signer.role,
|
||||||
|
signingOrder: signer.signingOrder,
|
||||||
|
fields: fields
|
||||||
|
.filter((field) => field.signerEmail === signer.email)
|
||||||
|
// 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,
|
||||||
|
pageX: f.pageX,
|
||||||
|
pageY: f.pageY,
|
||||||
|
width: f.pageWidth,
|
||||||
|
height: f.pageHeight,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _('Success'),
|
||||||
|
description: _('Document updated successfully'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a message to the parent window with the template details
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'template-updated',
|
||||||
|
// templateId: updateResult.templateId,
|
||||||
|
templateId: 1,
|
||||||
|
externalId: templateExternalId,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating template:', err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: _('Error'),
|
||||||
|
description: _('Failed to update template'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
try {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
||||||
|
JSON.parse(decodeURIComponent(atob(hash))),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeatures(result.data.features);
|
||||||
|
|
||||||
|
// Extract externalId from the parsed data if available
|
||||||
|
if (result.data.externalId) {
|
||||||
|
setExternalId(result.data.externalId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing embedding params:', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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
|
||||||
|
defaultValues={configuration ?? undefined}
|
||||||
|
disableUpload={true}
|
||||||
|
onSubmit={handleConfigurePageViewSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfigureFieldsView
|
||||||
|
configData={configuration!}
|
||||||
|
documentData={template.templateDocumentData}
|
||||||
|
defaultValues={fields ?? undefined}
|
||||||
|
onBack={handleBackToConfig}
|
||||||
|
onSubmit={handleConfigureFieldsSubmit}
|
||||||
|
/>
|
||||||
|
</Stepper>
|
||||||
|
</ConfigureDocumentProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import { useLayoutEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { Outlet } from 'react-router';
|
|
||||||
|
|
||||||
import { TrpcProvider, trpc } from '@documenso/trpc/react';
|
|
||||||
|
|
||||||
import { EmbedClientLoading } from '~/components/embed/embed-client-loading';
|
|
||||||
import { ZBaseEmbedAuthoringSchema } from '~/types/embed-authoring-base-schema';
|
|
||||||
import { injectCss } from '~/utils/css-vars';
|
|
||||||
|
|
||||||
export default function AuthoringLayout() {
|
|
||||||
const [token, setToken] = useState('');
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: verifyEmbeddingPresignToken,
|
|
||||||
isPending: isVerifyingEmbeddingPresignToken,
|
|
||||||
data: isVerified,
|
|
||||||
} = trpc.embeddingPresign.verifyEmbeddingPresignToken.useMutation();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
try {
|
|
||||||
const hash = window.location.hash.slice(1);
|
|
||||||
|
|
||||||
const result = ZBaseEmbedAuthoringSchema.safeParse(
|
|
||||||
JSON.parse(decodeURIComponent(atob(hash))),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token, css, cssVars, darkModeDisabled } = result.data;
|
|
||||||
|
|
||||||
if (darkModeDisabled) {
|
|
||||||
document.documentElement.classList.add('dark-mode-disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
injectCss({
|
|
||||||
css,
|
|
||||||
cssVars,
|
|
||||||
});
|
|
||||||
|
|
||||||
void verifyEmbeddingPresignToken({ token }).then((result) => {
|
|
||||||
if (result.success) {
|
|
||||||
setToken(token);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error verifying embedding presign token:', err);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isVerifyingEmbeddingPresignToken) {
|
|
||||||
return <EmbedClientLoading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof isVerified !== 'undefined' && !isVerified.success) {
|
|
||||||
return <div>Invalid embedding presign token</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TrpcProvider headers={{ authorization: `Bearer ${token}` }}>
|
|
||||||
<Outlet />
|
|
||||||
</TrpcProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -17,6 +17,8 @@ export const VALID_DATE_FORMAT_VALUES = [
|
|||||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export type ValidDateFormat = (typeof VALID_DATE_FORMAT_VALUES)[number];
|
||||||
|
|
||||||
export const DATE_FORMATS = [
|
export const DATE_FORMATS = [
|
||||||
{
|
{
|
||||||
key: 'yyyy-MM-dd_hh:mm_a',
|
key: 'yyyy-MM-dd_hh:mm_a',
|
||||||
@ -94,3 +96,7 @@ export const convertToLocalSystemFormat = (
|
|||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isValidDateFormat = (dateFormat: unknown): dateFormat is ValidDateFormat => {
|
||||||
|
return VALID_DATE_FORMAT_VALUES.includes(dateFormat as ValidDateFormat);
|
||||||
|
};
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
typedSignatureEnabled: z.boolean(),
|
typedSignatureEnabled: z.boolean(),
|
||||||
uploadSignatureEnabled: z.boolean(),
|
uploadSignatureEnabled: z.boolean(),
|
||||||
drawSignatureEnabled: z.boolean(),
|
drawSignatureEnabled: z.boolean(),
|
||||||
|
allowEmbeddedAuthoring: z.boolean(),
|
||||||
})
|
})
|
||||||
.nullish(),
|
.nullish(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -252,7 +252,10 @@ export const setDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return upsertedRecipient;
|
return {
|
||||||
|
...upsertedRecipient,
|
||||||
|
clientId: recipient.clientId,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -332,7 +335,7 @@ export const setDocumentRecipients = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter out recipients that have been removed or have been updated.
|
// Filter out recipients that have been removed or have been updated.
|
||||||
const filteredRecipients: Recipient[] = existingRecipients.filter((recipient) => {
|
const filteredRecipients: RecipientDataWithClientId[] = existingRecipients.filter((recipient) => {
|
||||||
const isRemoved = removedRecipients.find(
|
const isRemoved = removedRecipients.find(
|
||||||
(removedRecipient) => removedRecipient.id === recipient.id,
|
(removedRecipient) => removedRecipient.id === recipient.id,
|
||||||
);
|
);
|
||||||
@ -353,6 +356,7 @@ export const setDocumentRecipients = async ({
|
|||||||
*/
|
*/
|
||||||
type RecipientData = {
|
type RecipientData = {
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
|
clientId?: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
@ -361,6 +365,10 @@ type RecipientData = {
|
|||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RecipientDataWithClientId = Recipient & {
|
||||||
|
clientId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "allowEmbeddedAuthoring" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@ -565,6 +565,8 @@ model TeamGlobalSettings {
|
|||||||
brandingCompanyDetails String @default("")
|
brandingCompanyDetails String @default("")
|
||||||
brandingHidePoweredBy Boolean @default(false)
|
brandingHidePoweredBy Boolean @default(false)
|
||||||
|
|
||||||
|
allowEmbeddedAuthoring Boolean @default(false)
|
||||||
|
|
||||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { router } from '../trpc';
|
|||||||
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
||||||
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
||||||
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
||||||
import { getEmbeddingDocumentRoute } from './get-embedding-document';
|
import { updateEmbeddingDocumentRoute } from './update-embedding-document';
|
||||||
|
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
|
||||||
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
||||||
|
|
||||||
export const embeddingPresignRouter = router({
|
export const embeddingPresignRouter = router({
|
||||||
@ -10,5 +11,6 @@ export const embeddingPresignRouter = router({
|
|||||||
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
|
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
|
||||||
createEmbeddingDocument: createEmbeddingDocumentRoute,
|
createEmbeddingDocument: createEmbeddingDocumentRoute,
|
||||||
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
||||||
getEmbeddingDocument: getEmbeddingDocumentRoute,
|
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
||||||
|
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
|
import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
|
||||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { procedure } from '../trpc';
|
import { procedure } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -42,13 +42,24 @@ export const createEmbeddingPresignTokenRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([
|
const [hasCommunityPlan, hasEnterprisePlan] = await Promise.all([
|
||||||
isCommunityPlan({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
isCommunityPlan({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
||||||
isDocumentPlatform({ userId: token.userId, teamId: token.teamId }),
|
|
||||||
isUserEnterprise({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
isUserEnterprise({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!hasCommunityPlan && !hasPlatformPlan && !hasEnterprisePlan) {
|
let hasTeamAuthoringFlag = false;
|
||||||
|
|
||||||
|
if (token.teamId) {
|
||||||
|
const teamGlobalSettings = await prisma.teamGlobalSettings.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: token.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
hasTeamAuthoringFlag = teamGlobalSettings?.allowEmbeddedAuthoring ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCommunityPlan && !hasEnterprisePlan && !hasTeamAuthoringFlag) {
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
message: 'You do not have permission to create embedding presign tokens',
|
message: 'You do not have permission to create embedding presign tokens',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { procedure } from '../trpc';
|
|
||||||
import {
|
|
||||||
ZGetEmbeddingDocumentRequestSchema,
|
|
||||||
ZGetEmbeddingDocumentResponseSchema,
|
|
||||||
} from './get-embedding-document.types';
|
|
||||||
|
|
||||||
export const getEmbeddingDocumentRoute = procedure
|
|
||||||
.input(ZGetEmbeddingDocumentRequestSchema)
|
|
||||||
.output(ZGetEmbeddingDocumentResponseSchema)
|
|
||||||
.query(async ({ input, ctx: { req } }) => {
|
|
||||||
try {
|
|
||||||
const authorizationHeader = req.headers.get('authorization');
|
|
||||||
|
|
||||||
const [presignToken] = (authorizationHeader || '')
|
|
||||||
.split('Bearer ')
|
|
||||||
.filter((s) => s.length > 0);
|
|
||||||
|
|
||||||
if (!presignToken) {
|
|
||||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
||||||
message: 'No presign token provided',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
|
||||||
|
|
||||||
const { documentId } = input;
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
userId: apiToken.userId,
|
|
||||||
...(apiToken.teamId ? { teamId: apiToken.teamId } : {}),
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentData: true,
|
|
||||||
recipients: true,
|
|
||||||
fields: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
||||||
message: 'Document not found',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
document,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
|
||||||
message: 'Failed to get document',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { DocumentDataType, type Field, type Recipient } from '@prisma/client';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const ZGetEmbeddingDocumentRequestSchema = z.object({
|
|
||||||
documentId: z.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZGetEmbeddingDocumentResponseSchema = z.object({
|
|
||||||
document: z
|
|
||||||
.object({
|
|
||||||
id: z.number(),
|
|
||||||
title: z.string(),
|
|
||||||
status: z.string(),
|
|
||||||
documentDataId: z.string(),
|
|
||||||
userId: z.number(),
|
|
||||||
teamId: z.number().nullable(),
|
|
||||||
createdAt: z.date(),
|
|
||||||
updatedAt: z.date(),
|
|
||||||
documentData: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
type: z.nativeEnum(DocumentDataType),
|
|
||||||
data: z.string(),
|
|
||||||
initialData: z.string(),
|
|
||||||
}),
|
|
||||||
recipients: z.array(z.custom<Recipient>()),
|
|
||||||
fields: z.array(z.custom<Field>()),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TGetEmbeddingDocumentRequestSchema = z.infer<typeof ZGetEmbeddingDocumentRequestSchema>;
|
|
||||||
export type TGetEmbeddingDocumentResponseSchema = z.infer<
|
|
||||||
typeof ZGetEmbeddingDocumentResponseSchema
|
|
||||||
>;
|
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||||
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
|
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
|
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||||
|
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
||||||
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
|
|
||||||
|
import { procedure } from '../trpc';
|
||||||
|
import {
|
||||||
|
ZUpdateEmbeddingDocumentRequestSchema,
|
||||||
|
ZUpdateEmbeddingDocumentResponseSchema,
|
||||||
|
} from './update-embedding-document.types';
|
||||||
|
|
||||||
|
export const updateEmbeddingDocumentRoute = procedure
|
||||||
|
.input(ZUpdateEmbeddingDocumentRequestSchema)
|
||||||
|
.output(ZUpdateEmbeddingDocumentResponseSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||||
|
|
||||||
|
const [presignToken] = (authorizationHeader || '')
|
||||||
|
.split('Bearer ')
|
||||||
|
.filter((s) => s.length > 0);
|
||||||
|
|
||||||
|
if (!presignToken) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'No presign token provided',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||||
|
|
||||||
|
const { documentId, title, externalId, recipients, meta } = input;
|
||||||
|
|
||||||
|
if (meta && Object.values(meta).length > 0) {
|
||||||
|
await upsertDocumentMeta({
|
||||||
|
documentId: documentId,
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
...meta,
|
||||||
|
requestMetadata: ctx.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateDocument({
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
documentId: documentId,
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
externalId,
|
||||||
|
},
|
||||||
|
requestMetadata: ctx.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipientsWithClientId = recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
clientId: nanoid(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { recipients: updatedRecipients } = await setDocumentRecipients({
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
documentId: documentId,
|
||||||
|
recipients: recipientsWithClientId.map((recipient) => ({
|
||||||
|
id: recipient.id,
|
||||||
|
clientId: recipient.clientId,
|
||||||
|
email: recipient.email,
|
||||||
|
name: recipient.name ?? '',
|
||||||
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
})),
|
||||||
|
requestMetadata: ctx.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = recipientsWithClientId.flatMap((recipient) => {
|
||||||
|
const recipientId = updatedRecipients.find((r) => r.clientId === recipient.clientId)?.id;
|
||||||
|
|
||||||
|
if (!recipientId) {
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Recipient not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (recipient.fields ?? []).map((field) => ({
|
||||||
|
...field,
|
||||||
|
recipientId,
|
||||||
|
// !: Temp property to be removed once we don't link based on signer email
|
||||||
|
signerEmail: recipient.email,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
await setFieldsForDocument({
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
documentId,
|
||||||
|
fields: fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
pageWidth: field.width,
|
||||||
|
pageHeight: field.height,
|
||||||
|
})),
|
||||||
|
requestMetadata: ctx.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentId,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Failed to update document',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
|
import {
|
||||||
|
ZFieldHeightSchema,
|
||||||
|
ZFieldPageNumberSchema,
|
||||||
|
ZFieldPageXSchema,
|
||||||
|
ZFieldPageYSchema,
|
||||||
|
ZFieldWidthSchema,
|
||||||
|
} from '@documenso/lib/types/field';
|
||||||
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ZDocumentExternalIdSchema,
|
||||||
|
ZDocumentMetaDateFormatSchema,
|
||||||
|
ZDocumentMetaDistributionMethodSchema,
|
||||||
|
ZDocumentMetaDrawSignatureEnabledSchema,
|
||||||
|
ZDocumentMetaLanguageSchema,
|
||||||
|
ZDocumentMetaMessageSchema,
|
||||||
|
ZDocumentMetaRedirectUrlSchema,
|
||||||
|
ZDocumentMetaSubjectSchema,
|
||||||
|
ZDocumentMetaTimezoneSchema,
|
||||||
|
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||||
|
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||||
|
ZDocumentTitleSchema,
|
||||||
|
} from '../document-router/schema';
|
||||||
|
|
||||||
|
export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
title: ZDocumentTitleSchema,
|
||||||
|
externalId: ZDocumentExternalIdSchema.optional(),
|
||||||
|
recipients: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
email: z.string().toLowerCase().email().min(1),
|
||||||
|
name: z.string(),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
|
signingOrder: z.number().optional(),
|
||||||
|
fields: ZFieldAndMetaSchema.and(
|
||||||
|
z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
pageNumber: ZFieldPageNumberSchema,
|
||||||
|
pageX: ZFieldPageXSchema,
|
||||||
|
pageY: ZFieldPageYSchema,
|
||||||
|
width: ZFieldWidthSchema,
|
||||||
|
height: ZFieldHeightSchema,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(recipients) => {
|
||||||
|
const emails = recipients.map((recipient) => recipient.email);
|
||||||
|
|
||||||
|
return new Set(emails).size === emails.length;
|
||||||
|
},
|
||||||
|
{ message: 'Recipients must have unique emails' },
|
||||||
|
),
|
||||||
|
meta: z
|
||||||
|
.object({
|
||||||
|
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||||
|
message: ZDocumentMetaMessageSchema.optional(),
|
||||||
|
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||||
|
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||||
|
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||||
|
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||||
|
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||||
|
language: ZDocumentMetaLanguageSchema.optional(),
|
||||||
|
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||||
|
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||||
|
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZUpdateEmbeddingDocumentResponseSchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUpdateEmbeddingDocumentRequestSchema = z.infer<
|
||||||
|
typeof ZUpdateEmbeddingDocumentRequestSchema
|
||||||
|
>;
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
|
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
|
||||||
|
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
|
||||||
|
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
|
||||||
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
|
|
||||||
|
import { procedure } from '../trpc';
|
||||||
|
import {
|
||||||
|
ZUpdateEmbeddingTemplateRequestSchema,
|
||||||
|
ZUpdateEmbeddingTemplateResponseSchema,
|
||||||
|
} from './update-embedding-template.types';
|
||||||
|
|
||||||
|
export const updateEmbeddingTemplateRoute = procedure
|
||||||
|
.input(ZUpdateEmbeddingTemplateRequestSchema)
|
||||||
|
.output(ZUpdateEmbeddingTemplateResponseSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||||
|
|
||||||
|
const [presignToken] = (authorizationHeader || '')
|
||||||
|
.split('Bearer ')
|
||||||
|
.filter((s) => s.length > 0);
|
||||||
|
|
||||||
|
if (!presignToken) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'No presign token provided',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||||
|
|
||||||
|
const { templateId, title, externalId, recipients, meta } = input;
|
||||||
|
|
||||||
|
await updateTemplate({
|
||||||
|
templateId,
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
externalId,
|
||||||
|
},
|
||||||
|
meta,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipientsWithClientId = recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
clientId: nanoid(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { recipients: updatedRecipients } = await setTemplateRecipients({
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
templateId,
|
||||||
|
recipients: recipientsWithClientId.map((recipient) => ({
|
||||||
|
id: recipient.id,
|
||||||
|
email: recipient.email,
|
||||||
|
name: recipient.name ?? '',
|
||||||
|
role: recipient.role ?? 'SIGNER',
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields = recipientsWithClientId.flatMap((recipient) => {
|
||||||
|
const recipientId = updatedRecipients.find((r) => r.email === recipient.email)?.id;
|
||||||
|
|
||||||
|
if (!recipientId) {
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Recipient not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (recipient.fields ?? []).map((field) => ({
|
||||||
|
...field,
|
||||||
|
recipientId,
|
||||||
|
// !: Temp property to be removed once we don't link based on signer email
|
||||||
|
signerEmail: recipient.email,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
await setFieldsForTemplate({
|
||||||
|
userId: apiToken.userId,
|
||||||
|
teamId: apiToken.teamId ?? undefined,
|
||||||
|
templateId,
|
||||||
|
fields: fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
pageWidth: field.width,
|
||||||
|
pageHeight: field.height,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
templateId,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Failed to update template',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
|
import {
|
||||||
|
ZFieldHeightSchema,
|
||||||
|
ZFieldPageNumberSchema,
|
||||||
|
ZFieldPageXSchema,
|
||||||
|
ZFieldPageYSchema,
|
||||||
|
ZFieldWidthSchema,
|
||||||
|
} from '@documenso/lib/types/field';
|
||||||
|
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ZDocumentMetaDateFormatSchema,
|
||||||
|
ZDocumentMetaDistributionMethodSchema,
|
||||||
|
ZDocumentMetaDrawSignatureEnabledSchema,
|
||||||
|
ZDocumentMetaLanguageSchema,
|
||||||
|
ZDocumentMetaMessageSchema,
|
||||||
|
ZDocumentMetaRedirectUrlSchema,
|
||||||
|
ZDocumentMetaSubjectSchema,
|
||||||
|
ZDocumentMetaTimezoneSchema,
|
||||||
|
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||||
|
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||||
|
ZDocumentTitleSchema,
|
||||||
|
} from '../document-router/schema';
|
||||||
|
|
||||||
|
const ZFieldSchema = z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
type: z.nativeEnum(FieldType),
|
||||||
|
pageNumber: ZFieldPageNumberSchema,
|
||||||
|
pageX: ZFieldPageXSchema,
|
||||||
|
pageY: ZFieldPageYSchema,
|
||||||
|
width: ZFieldWidthSchema,
|
||||||
|
height: ZFieldHeightSchema,
|
||||||
|
fieldMeta: ZFieldMetaSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
||||||
|
templateId: z.number(),
|
||||||
|
title: ZDocumentTitleSchema.optional(),
|
||||||
|
externalId: z.string().optional(),
|
||||||
|
recipients: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.number().optional(),
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
role: z.nativeEnum(RecipientRole).optional(),
|
||||||
|
signingOrder: z.number().optional(),
|
||||||
|
fields: z.array(ZFieldSchema).optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
meta: z
|
||||||
|
.object({
|
||||||
|
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||||
|
message: ZDocumentMetaMessageSchema.optional(),
|
||||||
|
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||||
|
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||||
|
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||||
|
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||||
|
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||||
|
language: ZDocumentMetaLanguageSchema.optional(),
|
||||||
|
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||||
|
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||||
|
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||||
|
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZUpdateEmbeddingTemplateResponseSchema = z.object({
|
||||||
|
templateId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TUpdateEmbeddingTemplateRequestSchema = z.infer<
|
||||||
|
typeof ZUpdateEmbeddingTemplateRequestSchema
|
||||||
|
>;
|
||||||
@ -30,7 +30,7 @@ export const ZCreateRecipientSchema = z.object({
|
|||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ZUpdateRecipientSchema = z.object({
|
export const ZUpdateRecipientSchema = z.object({
|
||||||
id: z.number().describe('The ID of the recipient to update.'),
|
id: z.number().describe('The ID of the recipient to update.'),
|
||||||
email: z.string().toLowerCase().email().min(1).optional(),
|
email: z.string().toLowerCase().email().min(1).optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useCallback, useState } from 'react';
|
|||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { RecipientRole, SendStatus } from '@prisma/client';
|
import { RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
|
||||||
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
import { Check, ChevronsUpDown, Info } from 'lucide-react';
|
||||||
import { sortBy } from 'remeda';
|
import { sortBy } from 'remeda';
|
||||||
|
|
||||||
@ -145,6 +145,7 @@ export const RecipientSelector = ({
|
|||||||
onSelectedRecipientChange(recipient);
|
onSelectedRecipientChange(recipient);
|
||||||
setShowRecipientsSelector(false);
|
setShowRecipientsSelector(false);
|
||||||
}}
|
}}
|
||||||
|
disabled={recipient.signingStatus !== SigningStatus.NOT_SIGNED}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn('text-foreground/70 truncate', {
|
className={cn('text-foreground/70 truncate', {
|
||||||
|
|||||||
Reference in New Issue
Block a user