mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
10 Commits
v1.5.6-rc.
...
feat/redir
| Author | SHA1 | Date | |
|---|---|---|---|
| ba208d9397 | |||
| 2336e70495 | |||
| 3c23624fdb | |||
| 785b0e9085 | |||
| 279e7583cc | |||
| bb15f2994a | |||
| 2d6c613e2d | |||
| f84b2fbed0 | |||
| cce0cdfbe2 | |||
| 9715dbfeaa |
@ -4,7 +4,14 @@ import { useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client';
|
||||
import type {
|
||||
DocumentData,
|
||||
Field,
|
||||
Recipient,
|
||||
Template,
|
||||
TemplateDocumentMeta,
|
||||
User,
|
||||
} from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -19,6 +26,8 @@ import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-
|
||||
import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types';
|
||||
import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients';
|
||||
import type { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types';
|
||||
import { AddTemplateSettingsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-settings';
|
||||
import type { TTemplateSettingsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-settings.types';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type EditTemplateFormProps = {
|
||||
@ -28,11 +37,12 @@ export type EditTemplateFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
documentData: DocumentData;
|
||||
documentMeta: TemplateDocumentMeta | null;
|
||||
templateRootPath: string;
|
||||
};
|
||||
|
||||
type EditTemplateStep = 'signers' | 'fields';
|
||||
const EditTemplateSteps: EditTemplateStep[] = ['signers', 'fields'];
|
||||
type EditTemplateStep = 'settings' | 'signers' | 'fields';
|
||||
const EditTemplateSteps: EditTemplateStep[] = ['settings', 'signers', 'fields'];
|
||||
|
||||
export const EditTemplateForm = ({
|
||||
className,
|
||||
@ -42,22 +52,28 @@ export const EditTemplateForm = ({
|
||||
user: _user,
|
||||
documentData,
|
||||
templateRootPath,
|
||||
documentMeta,
|
||||
}: EditTemplateFormProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const [step, setStep] = useState<EditTemplateStep>('signers');
|
||||
const [step, setStep] = useState<EditTemplateStep>('settings');
|
||||
|
||||
const documentFlow: Record<EditTemplateStep, DocumentFlowStep> = {
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
description: 'Configure general settings for the template.',
|
||||
stepIndex: 1,
|
||||
},
|
||||
signers: {
|
||||
title: 'Add Placeholders',
|
||||
description: 'Add all relevant placeholders for each recipient.',
|
||||
stepIndex: 1,
|
||||
stepIndex: 2,
|
||||
},
|
||||
fields: {
|
||||
title: 'Add Fields',
|
||||
description: 'Add all relevant fields for each recipient.',
|
||||
stepIndex: 2,
|
||||
stepIndex: 3,
|
||||
},
|
||||
};
|
||||
|
||||
@ -65,6 +81,8 @@ export const EditTemplateForm = ({
|
||||
|
||||
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation();
|
||||
const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation();
|
||||
const { mutateAsync: setSettingsForTemplate } =
|
||||
trpc.template.setSettingsForTemplate.useMutation();
|
||||
|
||||
const onAddTemplatePlaceholderFormSubmit = async (
|
||||
data: TAddTemplatePlacholderRecipientsFormSchema,
|
||||
@ -87,6 +105,31 @@ export const EditTemplateForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddTemplateSettingsFormSubmit = async (data: TTemplateSettingsFormSchema) => {
|
||||
try {
|
||||
const { subject, message, timezone, dateFormat, redirectUrl } = data.meta;
|
||||
await setSettingsForTemplate({
|
||||
templateId: template.id,
|
||||
meta: {
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
},
|
||||
});
|
||||
router.refresh();
|
||||
setStep('signers');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while updating the template settings.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAddFieldsFormSubmit = async (data: TAddTemplateFieldsFormSchema) => {
|
||||
try {
|
||||
await addTemplateFields({
|
||||
@ -135,6 +178,14 @@ export const EditTemplateForm = ({
|
||||
currentStep={currentDocumentFlow.stepIndex}
|
||||
setCurrentStep={(step) => setStep(EditTemplateSteps[step - 1])}
|
||||
>
|
||||
<AddTemplateSettingsFormPartial
|
||||
template={template}
|
||||
key={recipients.length}
|
||||
documentFlow={documentFlow.settings}
|
||||
recipients={recipients}
|
||||
onSubmit={onAddTemplateSettingsFormSubmit}
|
||||
documentMeta={documentMeta}
|
||||
/>
|
||||
<AddTemplatePlaceholderRecipientsFormPartial
|
||||
key={recipients.length}
|
||||
documentFlow={documentFlow.signers}
|
||||
|
||||
@ -44,7 +44,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
||||
redirect(templateRootPath);
|
||||
}
|
||||
|
||||
const { templateDocumentData } = template;
|
||||
const { templateDocumentData, templateDocumentMeta } = template;
|
||||
|
||||
const [templateRecipients, templateFields] = await Promise.all([
|
||||
getRecipientsForTemplate({
|
||||
@ -79,6 +79,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
||||
recipients={templateRecipients}
|
||||
fields={templateFields}
|
||||
documentData={templateDocumentData}
|
||||
documentMeta={templateDocumentMeta}
|
||||
templateRootPath={templateRootPath}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -131,7 +131,7 @@ export default async function CompletedSigningPage({
|
||||
</div>
|
||||
))
|
||||
.with({ deletedAt: null }, () => (
|
||||
<div className="flex items-center mt-4 text-center text-blue-600">
|
||||
<div className="mt-4 flex items-center text-center text-blue-600">
|
||||
<Clock8 className="mr-2 h-5 w-5" />
|
||||
<span className="text-sm">Waiting for others to sign</span>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||
import { RefObject, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Calculate the width and height of a text element.
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { Field } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
|
||||
export const useFieldPageCoords = (field: Field) => {
|
||||
const [coords, setCoords] = useState({
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '@documenso/lib/constants/feature-flags';
|
||||
import { getAllFlags } from '@documenso/lib/universal/get-feature-flag';
|
||||
|
||||
import { TFeatureFlagValue } from './feature-flag.types';
|
||||
import type { TFeatureFlagValue } from './feature-flag.types';
|
||||
|
||||
export type FeatureFlagContextValue = {
|
||||
getFlag: (_key: string) => TFeatureFlagValue;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Toast } from '@documenso/ui/primitives/use-toast';
|
||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export const TOAST_DOCUMENT_SHARE_SUCCESS: Toast = {
|
||||
title: 'Copied to clipboard',
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Role, User } from '@documenso/prisma/client';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { Role } from '@documenso/prisma/client';
|
||||
|
||||
export const isAdmin = (user: User) => user.roles.includes(Role.ADMIN);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||
import { symmetricDecrypt } from '../../universal/crypto';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { User } from '@documenso/prisma/client';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
|
||||
import { getBackupCodes } from './get-backup-code';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { NextApiResponse } from 'next';
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextApiResponse } from 'next';
|
||||
import type { NextResponse } from 'next/server';
|
||||
|
||||
type NarrowedResponse<T> = T extends NextResponse
|
||||
? NextResponse
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type CreateTemplateDocumentMetaOptions = {
|
||||
templateId: number;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
timezone?: string;
|
||||
password?: string;
|
||||
dateFormat?: string;
|
||||
redirectUrl?: string;
|
||||
};
|
||||
|
||||
export const upsertTemplateDocumentMeta = async ({
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
dateFormat,
|
||||
templateId,
|
||||
password,
|
||||
redirectUrl,
|
||||
}: CreateTemplateDocumentMetaOptions) => {
|
||||
const templateDocumentMeta = await prisma.templateDocumentMeta.findFirstOrThrow({
|
||||
where: {
|
||||
templateId: templateId,
|
||||
},
|
||||
include: {
|
||||
template: true,
|
||||
},
|
||||
});
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const upsertedTemplateDocumentMeta = await tx.templateDocumentMeta.upsert({
|
||||
where: {
|
||||
templateId,
|
||||
},
|
||||
update: {
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
password,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
},
|
||||
create: {
|
||||
templateId,
|
||||
subject,
|
||||
message,
|
||||
timezone,
|
||||
password,
|
||||
dateFormat,
|
||||
redirectUrl,
|
||||
},
|
||||
});
|
||||
|
||||
return upsertedTemplateDocumentMeta;
|
||||
});
|
||||
};
|
||||
@ -65,6 +65,7 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
},
|
||||
templateDocumentData: true,
|
||||
templateDocumentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -128,6 +129,15 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
documentMeta: {
|
||||
where: {
|
||||
subject: template.templateDocumentMeta?.subject,
|
||||
message: template.templateDocumentMeta?.message,
|
||||
dateFormat: template.templateDocumentMeta?.dateFormat,
|
||||
timezone: template.templateDocumentMeta?.timezone,
|
||||
redirectUrl: template.templateDocumentMeta?.redirectUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ export const duplicateTemplate = async ({
|
||||
Recipient: true,
|
||||
Field: true,
|
||||
templateDocumentData: true,
|
||||
templateDocumentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ export const findTemplates = async ({
|
||||
where: whereFilter,
|
||||
include: {
|
||||
templateDocumentData: true,
|
||||
templateDocumentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -29,6 +29,7 @@ export const getTemplateById = async ({ id, userId }: GetTemplateByIdOptions) =>
|
||||
where: whereFilter,
|
||||
include: {
|
||||
templateDocumentData: true,
|
||||
templateDocumentMeta: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
|
||||
import type { TForgotPasswordFormSchema } from '@documenso/trpc/server/profile-router/schema';
|
||||
|
||||
import { ONE_DAY, ONE_HOUR } from '../../constants/time';
|
||||
import { sendForgotPassword } from '../auth/send-forgot-password';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field } from '@documenso/prisma/client';
|
||||
import type { Field } from '@documenso/prisma/client';
|
||||
|
||||
/**
|
||||
* Sort the fields by the Y position on the document.
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "TemplateDocumentMeta" (
|
||||
"id" TEXT NOT NULL,
|
||||
"subject" TEXT,
|
||||
"message" TEXT,
|
||||
"timezone" TEXT DEFAULT 'Etc/UTC',
|
||||
"password" TEXT,
|
||||
"dateFormat" TEXT DEFAULT 'yyyy-MM-dd hh:mm a',
|
||||
"templateId" INTEGER NOT NULL,
|
||||
"redirectUrl" TEXT,
|
||||
|
||||
CONSTRAINT "TemplateDocumentMeta_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TemplateDocumentMeta_templateId_key" ON "TemplateDocumentMeta"("templateId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TemplateDocumentMeta" ADD CONSTRAINT "TemplateDocumentMeta_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -324,6 +324,18 @@ model DocumentMeta {
|
||||
redirectUrl String?
|
||||
}
|
||||
|
||||
model TemplateDocumentMeta {
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
templateId Int @unique
|
||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
redirectUrl String?
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
NOT_OPENED
|
||||
OPENED
|
||||
@ -551,6 +563,7 @@ model Template {
|
||||
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
||||
templateDocumentMeta TemplateDocumentMeta?
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
Recipient Recipient[]
|
||||
Field Field[]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { TRPCClientError } from '@trpc/client';
|
||||
|
||||
import { AppRouter } from '../server/router';
|
||||
import type { AppRouter } from '../server/router';
|
||||
|
||||
export const isTRPCBadRequestError = (err: unknown): err is TRPCClientError<AppRouter> => {
|
||||
return err instanceof TRPCClientError && err.shape?.code === 'BAD_REQUEST';
|
||||
|
||||
@ -3,6 +3,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { upsertTemplateDocumentMeta } from '@documenso/lib/server-only/template-document-meta/upsert-template-document-meta';
|
||||
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
||||
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
@ -16,6 +17,7 @@ import {
|
||||
ZCreateTemplateMutationSchema,
|
||||
ZDeleteTemplateMutationSchema,
|
||||
ZDuplicateTemplateMutationSchema,
|
||||
ZSetSettingsForTemplateMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
export const templateRouter = router({
|
||||
@ -123,4 +125,27 @@ export const templateRouter = router({
|
||||
});
|
||||
}
|
||||
}),
|
||||
setSettingsForTemplate: authenticatedProcedure
|
||||
.input(ZSetSettingsForTemplateMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const { meta, templateId } = input;
|
||||
|
||||
return await upsertTemplateDocumentMeta({
|
||||
templateId,
|
||||
subject: meta.subject,
|
||||
message: meta.message,
|
||||
dateFormat: meta.dateFormat,
|
||||
timezone: meta.timezone,
|
||||
redirectUrl: meta.redirectUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'We were unable to set template settings. Please try again later.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
|
||||
export const ZCreateTemplateMutationSchema = z.object({
|
||||
title: z.string().min(1).trim(),
|
||||
teamId: z.number().optional(),
|
||||
@ -24,6 +26,22 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
sendDocument: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const ZSetSettingsForTemplateMutationSchema = z.object({
|
||||
templateId: z.number(),
|
||||
meta: z.object({
|
||||
subject: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
dateFormat: z.string().optional(),
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDuplicateTemplateMutationSchema = z.object({
|
||||
templateId: z.number(),
|
||||
teamId: z.number().optional(),
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
|
||||
@ -4,7 +4,8 @@ import { Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { Button } from './button';
|
||||
import { Input, InputProps } from './input';
|
||||
import type { InputProps } from './input';
|
||||
import { Input } from './input';
|
||||
|
||||
const PasswordInput = React.forwardRef<HTMLInputElement, Omit<InputProps, 'type'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
PointerEvent as ReactPointerEvent,
|
||||
TouchEvent as ReactTouchEvent,
|
||||
|
||||
281
packages/ui/primitives/template-flow/add-template-settings.tsx
Normal file
281
packages/ui/primitives/template-flow/add-template-settings.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||
import type { Template, TemplateDocumentMeta } from '@documenso/prisma/client';
|
||||
import { type Recipient, SendStatus } from '@documenso/prisma/client';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@documenso/ui/primitives/accordion';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
|
||||
import { Combobox } from '../combobox';
|
||||
import {
|
||||
DocumentFlowFormContainerActions,
|
||||
DocumentFlowFormContainerContent,
|
||||
DocumentFlowFormContainerFooter,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from '../document-flow/document-flow-root';
|
||||
import type { DocumentFlowStep } from '../document-flow/types';
|
||||
import { Input } from '../input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
||||
import { useStep } from '../stepper';
|
||||
import { Textarea } from '../textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||
import type { TTemplateSettingsFormSchema } from './add-template-settings.types';
|
||||
import { ZTemplateSettingsFormSchema } from './add-template-settings.types';
|
||||
|
||||
export type AddSettingsFormProps = {
|
||||
template: Template;
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
documentMeta: TemplateDocumentMeta | null;
|
||||
onSubmit: (_data: TTemplateSettingsFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddTemplateSettingsFormPartial = ({
|
||||
documentFlow,
|
||||
recipients,
|
||||
documentMeta,
|
||||
template,
|
||||
onSubmit,
|
||||
}: AddSettingsFormProps) => {
|
||||
const form = useForm<TTemplateSettingsFormSchema>({
|
||||
resolver: zodResolver(ZTemplateSettingsFormSchema),
|
||||
defaultValues: {
|
||||
title: template.title,
|
||||
meta: {
|
||||
subject: documentMeta?.subject ?? '',
|
||||
message: documentMeta?.message ?? '',
|
||||
timezone: documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
dateFormat: documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
redirectUrl: documentMeta?.redirectUrl ?? '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { stepIndex, currentStep, totalSteps, previousStep } = useStep();
|
||||
|
||||
const documentHasBeenSent = recipients.some(
|
||||
(recipient) => recipient.sendStatus === SendStatus.SENT,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!form.formState.touchedFields.meta?.timezone && !documentHasBeenSent) {
|
||||
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}
|
||||
}, [documentHasBeenSent, form, form.setValue, form.formState.touchedFields.meta?.timezone]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<Form {...form}>
|
||||
<fieldset
|
||||
className="flex h-full flex-col space-y-6"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} disabled={field.disabled} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Accordion type="multiple" className="mt-6">
|
||||
<AccordionItem value="advanced-options" className="border-none">
|
||||
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
||||
Advanced Options
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6 ">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.dateFormat"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Date Format</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
disabled={documentHasBeenSent}
|
||||
>
|
||||
<SelectTrigger className="bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{DATE_FORMATS.map((format) => (
|
||||
<SelectItem key={format.key} value={format.value}>
|
||||
{format.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
Subject{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
Add email subject
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
id="subject"
|
||||
className="bg-background mt-2"
|
||||
disabled={field.disabled}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
Message{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
Add message to send in the email
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Textarea
|
||||
id="message"
|
||||
className="bg-background mt-2 h-24 resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.timezone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Time Zone</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Combobox
|
||||
className="bg-background"
|
||||
options={TIME_ZONES}
|
||||
{...field}
|
||||
onChange={(value) => value && field.onChange(value)}
|
||||
disabled={documentHasBeenSent}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta.redirectUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
Redirect URL{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
Add a URL to redirect the user to once the document is signed
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</fieldset>
|
||||
</Form>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
<DocumentFlowFormContainerFooter>
|
||||
<DocumentFlowFormContainerStep
|
||||
title={documentFlow.title}
|
||||
step={currentStep}
|
||||
maxStep={totalSteps}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerActions
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={form.formState.isSubmitting}
|
||||
canGoBack={stepIndex !== 0}
|
||||
onGoBackClick={previousStep}
|
||||
onGoNextClick={form.handleSubmit(onSubmit)}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
import {
|
||||
ZDocumentAccessAuthTypesSchema,
|
||||
ZDocumentActionAuthTypesSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
|
||||
export const ZMapNegativeOneToUndefinedSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (val === '-1') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
export const ZTemplateSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
globalActionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentActionAuthTypesSchema.optional(),
|
||||
),
|
||||
meta: z.object({
|
||||
subject: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||
redirectUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((value) => value === undefined || value === '' || URL_REGEX.test(value), {
|
||||
message: 'Please enter a valid URL',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TTemplateSettingsFormSchema = z.infer<typeof ZTemplateSettingsFormSchema>;
|
||||
Reference in New Issue
Block a user