feat: wip

This commit is contained in:
Ephraim Atta-Duncan
2023-09-11 09:45:03 +00:00
parent d4f6fa7dc4
commit 7b28ba968e
15 changed files with 902 additions and 8 deletions

View File

@ -0,0 +1,166 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Field, Recipient } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { AddTemplateFormPartial } from '@documenso/ui/primitives/document-flow/add-template-details';
import { TAddTemplateSchema } from '@documenso/ui/primitives/document-flow/add-template-details.types';
import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-template-fields';
import { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/document-flow/add-template-fields.types';
import {
DocumentFlowFormContainer,
DocumentFlowFormContainerHeader,
} from '@documenso/ui/primitives/document-flow/document-flow-root';
import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { toast } from '@documenso/ui/primitives/use-toast';
type TemplatePageFlowStep = 'details' | 'fields';
export type DocumentPageProps = {
params: {
id: string;
};
};
export default function TemplatePage() {
const router = useRouter();
const [uploadedFile, setUploadedFile] = useState<{ name: string; file: string } | null>();
const [step, setStep] = useState<TemplatePageFlowStep>('details');
const [fields, setFields] = useState<Field[]>([]);
const documentFlow: Record<TemplatePageFlowStep, DocumentFlowStep> = {
details: {
title: 'Add Details',
description: 'Add the name and description of your template.',
stepIndex: 1,
onSubmit: () => onAddTemplateDetailsFormSubmit,
},
fields: {
title: 'Add Fields',
description: 'Add all relevant fields for each recipient.',
stepIndex: 2,
onBackStep: () => setStep('details'),
onSubmit: () => onAddTemplateFieldsFormSubmit,
},
};
const currentDocumentFlow = documentFlow[step];
const onAddTemplateDetailsFormSubmit = (data: TAddTemplateSchema) => {
if (!uploadedFile) {
return;
}
router.refresh();
const templateDetails = {
document: uploadedFile,
...data,
};
console.log(templateDetails);
setStep('fields');
};
const onAddTemplateFieldsFormSubmit = (data: TAddTemplateFieldsFormSchema) => {
if (!uploadedFile) {
return;
}
console.log(data);
console.log('Submit fields in document flow');
};
const onFileDrop = async (file: File) => {
try {
const arrayBuffer = await file.arrayBuffer();
const base64String = Buffer.from(arrayBuffer).toString('base64');
setUploadedFile({
name: file.name,
file: `data:application/pdf;base64,${base64String}`,
});
} catch {
toast({
title: 'Something went wrong',
description: 'Please try again later.',
variant: 'destructive',
});
}
};
const placeholderRecipient: Recipient[] = [
{
id: -1,
documentId: -1,
email: '',
name: '',
token: '',
expired: null,
signedAt: null,
readStatus: 'OPENED',
signingStatus: 'NOT_SIGNED',
sendStatus: 'NOT_SENT',
},
];
return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<h1 className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl" title="Templates">
Templates
</h1>
<div className="mt-12 grid w-full grid-cols-12 gap-8">
<div className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7">
{uploadedFile ? (
<Card gradient>
<CardContent className="p-2">
<LazyPDFViewer document={uploadedFile.file} />
</CardContent>
</Card>
) : (
<DocumentDropzone className="h-[80vh] max-h-[60rem]" onDrop={onFileDrop} />
)}
</div>
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
<DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
<DocumentFlowFormContainerHeader
title={currentDocumentFlow.title}
description={currentDocumentFlow.description}
/>
{step === 'details' && (
<fieldset disabled={!uploadedFile} className="flex h-full flex-col">
<AddTemplateFormPartial
documentFlow={documentFlow.details}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddTemplateDetailsFormSubmit}
/>
</fieldset>
)}
{step === 'fields' && (
<AddTemplateFieldsFormPartial
documentFlow={documentFlow.fields}
recipients={placeholderRecipient}
fields={fields}
numberOfSteps={Object.keys(documentFlow).length}
onSubmit={onAddTemplateFieldsFormSubmit}
/>
)}
</DocumentFlowFormContainer>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
'use server';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { TAddSignersFormSchema } from '@documenso/ui/primitives/document-flow/add-signers.types';
export type AddSignersActionInput = TAddSignersFormSchema & {
documentId: number;
};
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
'use server';
const { id: userId } = await getRequiredServerComponentSession();
await setRecipientsForDocument({
userId,
documentId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
})),
});
};

View File

@ -0,0 +1,38 @@
'use server';
import { nanoid } from 'nanoid';
import { prisma } from '@documenso/prisma';
import { DocumentContent } from '@documenso/prisma/client';
type CreateTemplateInput = {
name: string;
description: string;
document: DocumentContent;
userId: number;
};
export const createTemplate = async ({
name,
description,
document,
userId,
}: CreateTemplateInput) => {
const createTemplateDocument = await prisma.documentContent.create({
data: {
content: document.content,
name: document.name,
},
});
const createdTemplate = await prisma.template.create({
data: {
name,
slug: nanoid(10),
description,
ownerId: userId,
documentId: createTemplateDocument.id,
},
});
return createdTemplate;
};

View File

@ -0,0 +1,34 @@
-- CreateEnum
CREATE TYPE "TemplateType" AS ENUM ('PRIVATE', 'PUBLIC');
-- CreateTable
CREATE TABLE "Template" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"kind" "TemplateType" NOT NULL,
"ownerId" INTEGER NOT NULL,
"documentId" INTEGER NOT NULL,
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DocumentContent" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
CONSTRAINT "DocumentContent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Template_slug_key" ON "Template"("slug");
-- CreateIndex
CREATE INDEX "Template_ownerId_idx" ON "Template"("ownerId");
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "DocumentContent"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,21 @@
/*
Warnings:
- You are about to drop the `DocumentContent` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Template` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_documentId_fkey";
-- DropForeignKey
ALTER TABLE "Template" DROP CONSTRAINT "Template_ownerId_fkey";
-- DropTable
DROP TABLE "DocumentContent";
-- DropTable
DROP TABLE "Template";
-- DropEnum
DROP TYPE "TemplateType";

View File

@ -0,0 +1,34 @@
-- CreateEnum
CREATE TYPE "TemplateType" AS ENUM ('PRIVATE', 'PUBLIC');
-- CreateTable
CREATE TABLE "Template" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"kind" "TemplateType" NOT NULL,
"ownerId" INTEGER NOT NULL,
"documentId" INTEGER NOT NULL,
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DocumentContent" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
CONSTRAINT "DocumentContent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Template_slug_key" ON "Template"("slug");
-- CreateIndex
CREATE INDEX "Template_ownerId_idx" ON "Template"("ownerId");
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "DocumentContent"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Template" ALTER COLUMN "kind" SET DEFAULT 'PRIVATE';

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Template" ADD COLUMN "description" TEXT;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `name` to the `DocumentContent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "DocumentContent" ADD COLUMN "name" TEXT NOT NULL;

View File

@ -177,7 +177,8 @@ model Template {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
slug String @unique slug String @unique
kind TemplateType description String?
kind TemplateType @default(PRIVATE)
ownerId Int ownerId Int
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
documentId Int documentId Int
@ -188,6 +189,7 @@ model Template {
model DocumentContent { model DocumentContent {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String
content String content String
templates Template[] templates Template[]
} }

View File

@ -0,0 +1,103 @@
'use client';
import React from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Field } from '@documenso/prisma/client';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { TAddTemplateSchema, ZAddTemplateSchema } from './add-template-details.types';
import {
DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter,
DocumentFlowFormContainerStep,
} from './document-flow-root';
import { DocumentFlowStep } from './types';
export type AddTemplateFormProps = {
documentFlow: DocumentFlowStep;
fields: Field[];
numberOfSteps: number;
onSubmit: (_data: TAddTemplateSchema) => void;
};
export const AddTemplateFormPartial = ({
documentFlow,
numberOfSteps,
fields: _fields,
onSubmit,
}: AddTemplateFormProps) => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<TAddTemplateSchema>({
resolver: zodResolver(ZAddTemplateSchema),
defaultValues: {
template: {
name: '',
description: '',
},
},
});
const onFormSubmit = handleSubmit(onSubmit);
return (
<>
<DocumentFlowFormContainerContent>
<div className="flex flex-col">
<div className="flex flex-col gap-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input
id="name"
className="bg-background mt-2"
disabled={isSubmitting}
{...register('template.name')}
/>
<FormErrorMessage className="mt-2" error={errors.template?.name} />
</div>
<div>
<Label htmlFor="description">
Description <span className="text-muted-foreground">(Optional)</span>
</Label>
<Input
id="description"
className="bg-background mt-2"
disabled={isSubmitting}
{...register('template.description')}
/>
<FormErrorMessage className="mt-2" error={errors.template?.description} />
</div>
</div>
</div>
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={documentFlow.stepIndex}
maxStep={numberOfSteps}
/>
<DocumentFlowFormContainerActions
loading={isSubmitting}
disabled={isSubmitting}
onGoBackClick={documentFlow.onBackStep}
onGoNextClick={() => void onFormSubmit()}
/>
</DocumentFlowFormContainerFooter>
</>
);
};

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZAddTemplateSchema = z.object({
template: z.object({
name: z.string(),
description: z.string(),
}),
});
export type TAddTemplateSchema = z.infer<typeof ZAddTemplateSchema>;

View File

@ -0,0 +1,418 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { nanoid } from 'nanoid';
import { useFieldArray, useForm } from 'react-hook-form';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { Field, FieldType, Recipient, SendStatus } from '@documenso/prisma/client';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
import {
DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter,
DocumentFlowFormContainerStep,
} from './document-flow-root';
import { FieldItem } from './field-item';
import { DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
const fontCaveat = Caveat({
weight: ['500'],
subsets: ['latin'],
display: 'swap',
variable: '--font-caveat',
});
const DEFAULT_HEIGHT_PERCENT = 5;
const DEFAULT_WIDTH_PERCENT = 15;
const MIN_HEIGHT_PX = 60;
const MIN_WIDTH_PX = 200;
export type AddTemplateFieldsFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
fields: Field[];
numberOfSteps: number;
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
};
export const AddTemplateFieldsFormPartial = ({
documentFlow,
recipients,
fields,
numberOfSteps,
onSubmit,
}: AddTemplateFieldsFormProps) => {
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
const {
control,
handleSubmit,
formState: { isSubmitting },
} = useForm<TAddTemplateFieldsFormSchema>({
defaultValues: {
fields: fields.map((field) => ({
nativeId: field.id,
formId: `${field.id}-${field.documentId}`,
pageNumber: field.page,
type: field.type,
pageX: Number(field.positionX),
pageY: Number(field.positionY),
pageWidth: Number(field.width),
pageHeight: Number(field.height),
signerEmail:
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
})),
},
});
const onFormSubmit = handleSubmit(onSubmit);
const {
append,
remove,
update,
fields: localFields,
} = useFieldArray({
control,
name: 'fields',
});
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
const [coords, setCoords] = useState({
x: 0,
y: 0,
});
const fieldBounds = useRef({
height: 0,
width: 0,
});
const onMouseMove = useCallback(
(event: MouseEvent) => {
setIsFieldWithinBounds(
isWithinPageBounds(
event,
PDF_VIEWER_PAGE_SELECTOR,
fieldBounds.current.width,
fieldBounds.current.height,
),
);
setCoords({
x: event.clientX - fieldBounds.current.width / 2,
y: event.clientY - fieldBounds.current.height / 2,
});
},
[isWithinPageBounds],
);
const onMouseClick = useCallback(
(event: MouseEvent) => {
if (!selectedField || !selectedSigner) {
return;
}
const $page = getPage(event, PDF_VIEWER_PAGE_SELECTOR);
if (
!$page ||
!isWithinPageBounds(
event,
PDF_VIEWER_PAGE_SELECTOR,
fieldBounds.current.width,
fieldBounds.current.height,
)
) {
setSelectedField(null);
return;
}
const { top, left, height, width } = getBoundingClientRect($page);
const pageNumber = parseInt($page.getAttribute('data-page-number') ?? '1', 10);
// Calculate x and y as a percentage of the page width and height
let pageX = ((event.pageX - left) / width) * 100;
let pageY = ((event.pageY - top) / height) * 100;
// Get the bounds as a percentage of the page width and height
const fieldPageWidth = (fieldBounds.current.width / width) * 100;
const fieldPageHeight = (fieldBounds.current.height / height) * 100;
// And center it based on the bounds
pageX -= fieldPageWidth / 2;
pageY -= fieldPageHeight / 2;
append({
formId: nanoid(12),
type: selectedField,
pageNumber,
pageX,
pageY,
pageWidth: fieldPageWidth,
pageHeight: fieldPageHeight,
signerEmail: selectedSigner.email,
});
setIsFieldWithinBounds(false);
setSelectedField(null);
},
[append, isWithinPageBounds, selectedField, selectedSigner, getPage],
);
const onFieldResize = useCallback(
(node: HTMLElement, index: number) => {
const field = localFields[index];
const $page = window.document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
);
if (!$page) {
return;
}
const {
x: pageX,
y: pageY,
width: pageWidth,
height: pageHeight,
} = getFieldPosition($page, node);
update(index, {
...field,
pageX,
pageY,
pageWidth,
pageHeight,
});
},
[getFieldPosition, localFields, update],
);
const onFieldMove = useCallback(
(node: HTMLElement, index: number) => {
const field = localFields[index];
const $page = window.document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
);
if (!$page) {
return;
}
const { x: pageX, y: pageY } = getFieldPosition($page, node);
update(index, {
...field,
pageX,
pageY,
});
},
[getFieldPosition, localFields, update],
);
useEffect(() => {
if (selectedField) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click', onMouseClick);
}
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('click', onMouseClick);
};
}, [onMouseClick, onMouseMove, selectedField]);
useEffect(() => {
const $page = window.document.querySelector(PDF_VIEWER_PAGE_SELECTOR);
if (!$page) {
return;
}
const { height, width } = $page.getBoundingClientRect();
fieldBounds.current = {
height: Math.max(height * (DEFAULT_HEIGHT_PERCENT / 100), MIN_HEIGHT_PX),
width: Math.max(width * (DEFAULT_WIDTH_PERCENT / 100), MIN_WIDTH_PX),
};
}, []);
useEffect(() => {
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
}, [recipients]);
return (
<>
<DocumentFlowFormContainerContent>
<div className="flex flex-col">
{selectedField && (
<Card
className={cn(
'pointer-events-none fixed z-50 cursor-pointer bg-white transition-opacity',
{
'border-primary': isFieldWithinBounds,
'opacity-50': !isFieldWithinBounds,
},
)}
style={{
top: coords.y,
left: coords.x,
height: fieldBounds.current.height,
width: fieldBounds.current.width,
}}
>
<CardContent className="text-foreground flex h-full w-full items-center justify-center p-2">
{FRIENDLY_FIELD_TYPE[selectedField]}
</CardContent>
</Card>
)}
{localFields.map((field, index) => (
<FieldItem
key={index}
field={field}
disabled={selectedSigner?.email !== field.signerEmail || hasSelectedSignerBeenSent}
minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width}
passive={isFieldWithinBounds && !!selectedField}
onResize={(options) => onFieldResize(options, index)}
onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)}
/>
))}
<div className="-mx-2 flex-1 overflow-y-scroll px-2">
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
<button
type="button"
className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-3xl font-medium',
fontCaveat.className,
)}
>
{selectedSigner?.name || 'Signature'}
</p>
<p className="text-muted-foreground mt-2 text-center text-xs">Signature</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
)}
>
{'Email'}
</p>
<p className="text-muted-foreground mt-2 text-xs">Email</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.NAME)}
data-selected={selectedField === FieldType.NAME ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
)}
>
{'Name'}
</p>
<p className="text-muted-foreground mt-2 text-xs">Name</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
onClick={(e) => e.stopPropagation()}
onMouseDown={() => setSelectedField(FieldType.DATE)}
data-selected={selectedField === FieldType.DATE ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
)}
>
{'Date'}
</p>
<p className="text-muted-foreground mt-2 text-xs">Date</p>
</CardContent>
</Card>
</button>
</div>
</div>
</div>
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={documentFlow.stepIndex}
maxStep={numberOfSteps}
/>
<DocumentFlowFormContainerActions
loading={isSubmitting}
disabled={isSubmitting}
onGoBackClick={documentFlow.onBackStep}
onGoNextClick={() => void onFormSubmit()}
/>
</DocumentFlowFormContainerFooter>
</>
);
};

View File

@ -0,0 +1,21 @@
import { z } from 'zod';
import { FieldType } from '@documenso/prisma/client';
export const ZAddTemplateFieldsFormSchema = z.object({
fields: z.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
type: z.nativeEnum(FieldType),
signerEmail: z.string().min(1),
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),
}),
),
});
export type TAddTemplateFieldsFormSchema = z.infer<typeof ZAddTemplateFieldsFormSchema>;

10
templates Normal file
View File

@ -0,0 +1,10 @@
Plan
Take the document
Take the details of the template
Take the fields
---??? Store it in the database with a templates schema
Issues?
Persist template details information.