diff --git a/apps/remix/app/components/forms/attachment-form.tsx b/apps/remix/app/components/forms/attachment-form.tsx new file mode 100644 index 000000000..0eb8734dc --- /dev/null +++ b/apps/remix/app/components/forms/attachment-form.tsx @@ -0,0 +1,184 @@ +import { useEffect } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Trash } from 'lucide-react'; +import { useFieldArray, useForm } from 'react-hook-form'; + +import type { TDocument } from '@documenso/lib/types/document'; +import { nanoid } from '@documenso/lib/universal/id'; +import { AttachmentType } from '@documenso/prisma/generated/types'; +import { trpc } from '@documenso/trpc/react'; +import type { TSetDocumentAttachmentsSchema } from '@documenso/trpc/server/attachment-router/schema'; +import { ZSetDocumentAttachmentsSchema } from '@documenso/trpc/server/attachment-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type AttachmentFormProps = { + document: TDocument; +}; + +export const AttachmentForm = ({ document }: AttachmentFormProps) => { + const { toast } = useToast(); + + const { data: attachmentsData, refetch: refetchAttachments } = + trpc.attachment.getAttachments.useQuery({ + documentId: document.id, + }); + + const { mutateAsync: setDocumentAttachments } = + trpc.attachment.setDocumentAttachments.useMutation(); + + const defaultAttachments = [ + { + id: nanoid(12), + label: '', + url: '', + type: AttachmentType.LINK, + }, + ]; + + const form = useForm({ + resolver: zodResolver(ZSetDocumentAttachmentsSchema), + defaultValues: { + documentId: document.id, + attachments: attachmentsData ?? defaultAttachments, + }, + }); + + const { + fields: attachments, + append: appendAttachment, + remove: removeAttachment, + } = useFieldArray({ + control: form.control, + name: 'attachments', + }); + + const onAddAttachment = () => { + appendAttachment({ + id: nanoid(12), + label: '', + url: '', + type: AttachmentType.LINK, + }); + }; + + const onRemoveAttachment = (index: number) => { + removeAttachment(index); + }; + + useEffect(() => { + if (attachmentsData) { + form.setValue('attachments', attachmentsData); + } + }, [attachmentsData]); + + const onSubmit = async (data: TSetDocumentAttachmentsSchema) => { + try { + await setDocumentAttachments({ + documentId: document.id, + attachments: data.attachments, + }); + + toast({ + title: 'Attachment(s) updated', + description: 'The attachment(s) have been updated successfully', + }); + + await refetchAttachments(); + } catch (error) { + console.error(error); + + toast({ + title: 'Something went wrong', + description: 'We encountered an unknown error while attempting to create the attachments.', + variant: 'destructive', + duration: 5000, + }); + } + }; + + return ( + + + + + + + Attachments + +
+ +
+ {attachments.map((attachment, index) => ( +
+ ( + + Label + + + + + + )} + /> + + ( + + URL + + + + + + )} + /> + + +
+ ))} +
+ + + + +
+ +
+
+ ); +}; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx index 2932de7ac..2ac9ea7ef 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.edit.tsx @@ -11,6 +11,7 @@ import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { AttachmentForm } from '~/components/forms/attachment-form'; import { DocumentEditForm } from '~/components/general/document/document-edit-form'; import { DocumentStatus } from '~/components/general/document/document-status'; import { LegacyFieldWarningPopover } from '~/components/general/legacy-field-warning-popover'; @@ -133,11 +134,18 @@ export default function DocumentEditPage() { - {document.useLegacyFieldInsertion && ( -
- -
- )} +
+ {document.useLegacyFieldInsertion ? ( +
+ + +
+ ) : ( +
+ +
+ )} +
; + +export const ZSetDocumentAttachmentsResponseSchema = z.array( + z.object({ + id: z.string(), + label: z.string(), + url: z.string(), + type: z.nativeEnum(AttachmentType), + }), +); diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index 861fd41ad..a171b1bc9 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -1,5 +1,6 @@ import { adminRouter } from './admin-router/router'; import { apiTokenRouter } from './api-token-router/router'; +import { attachmentRouter } from './attachment-router/router'; import { authRouter } from './auth-router/router'; import { billingRouter } from './billing/router'; import { documentRouter } from './document-router/router'; @@ -31,6 +32,7 @@ export const appRouter = router({ template: templateRouter, webhook: webhookRouter, embeddingPresign: embeddingPresignRouter, + attachment: attachmentRouter, }); export type AppRouter = typeof appRouter; diff --git a/packages/ui/primitives/document-flow/add-settings.tsx b/packages/ui/primitives/document-flow/add-settings.tsx index d3491bcea..3c06f9d1c 100644 --- a/packages/ui/primitives/document-flow/add-settings.tsx +++ b/packages/ui/primitives/document-flow/add-settings.tsx @@ -1,4 +1,4 @@ -import { useEffect, useId } from 'react'; +import { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans, useLingui } from '@lingui/react/macro'; @@ -10,8 +10,8 @@ import { SendStatus, TeamMemberRole, } from '@prisma/client'; -import { InfoIcon, Plus, Trash } from 'lucide-react'; -import { useFieldArray, useForm } from 'react-hook-form'; +import { InfoIcon } from 'lucide-react'; +import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; @@ -20,7 +20,6 @@ import { DOCUMENT_SIGNATURE_TYPES } from '@documenso/lib/constants/document'; import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import type { TDocument } from '@documenso/lib/types/document'; -import { nanoid } from '@documenso/lib/universal/id'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams'; import { @@ -45,7 +44,6 @@ import { AccordionItem, AccordionTrigger, } from '@documenso/ui/primitives/accordion'; -import { Button } from '@documenso/ui/primitives/button'; import { Form, FormControl, @@ -93,7 +91,6 @@ export const AddSettingsFormPartial = ({ onSubmit, }: AddSettingsFormProps) => { const { t } = useLingui(); - const initialId = useId(); const organisation = useCurrentOrganisation(); @@ -101,16 +98,6 @@ export const AddSettingsFormPartial = ({ documentAuth: document.authOptions, }); - const defaultAttachments = [ - { - id: '', - formId: initialId, - label: '', - url: '', - type: 'LINK', - }, - ]; - const form = useForm({ resolver: zodResolver(ZAddSettingsFormSchema), defaultValues: { @@ -131,55 +118,15 @@ export const AddSettingsFormPartial = ({ language: document.documentMeta?.language ?? 'en', signatureTypes: extractTeamSignatureSettings(document.documentMeta), }, - attachments: - document.attachments?.map((attachment) => ({ - ...attachment, - id: String(attachment.id), - formId: String(attachment.id), - })) ?? defaultAttachments, }, }); - const { - fields: attachments, - append: appendAttachment, - remove: removeAttachment, - } = useFieldArray({ - control: form.control, - name: 'attachments', - }); - - const onRemoveAttachment = (index: number) => { - const attachment = attachments[index]; - - const formStateIndex = - form.getValues('attachments')?.findIndex((a) => a.formId === attachment.formId) ?? -1; - - if (formStateIndex !== -1) { - removeAttachment(formStateIndex); - - const updatedAttachments = - form.getValues('attachments')?.filter((a) => a.formId !== attachment.formId) ?? []; - - form.setValue('attachments', updatedAttachments); - } - }; - const { stepIndex, currentStep, totalSteps, previousStep } = useStep(); const documentHasBeenSent = recipients.some( (recipient) => recipient.sendStatus === SendStatus.SENT, ); - const onAddAttachment = () => { - appendAttachment({ - id: nanoid(12), - formId: nanoid(12), - label: '', - url: '', - }); - }; - const canUpdateVisibility = match(currentTeamMemberRole) .with(TeamMemberRole.ADMIN, () => true) .with( @@ -526,81 +473,6 @@ export const AddSettingsFormPartial = ({ - - - - - Attachments - - - -
- {attachments.map((attachment, index) => ( -
-
- ( - - - Label - - - - - - - - - )} - /> -
- -
- ( - - - Location link - - - - - - - - - )} - /> -
- -
- -
-
- ))} - - -
-
-
-