diff --git a/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx b/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx index d210550c6..cd700ac1e 100644 --- a/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx @@ -114,7 +114,7 @@ export const TemplateBulkSendDialog = ({ {trigger ?? ( - diff --git a/apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx b/apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx index 27b01e963..13d1f58a4 100644 --- a/apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx +++ b/apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx @@ -9,6 +9,10 @@ import { z } from 'zod'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import type { TTemplate } from '@documenso/lib/types/template'; +import { + DocumentReadOnlyFields, + mapFieldsWithRecipients, +} from '@documenso/ui/components/document/document-read-only-fields'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, @@ -16,7 +20,6 @@ import { DocumentFlowFormContainerHeader, DocumentFlowFormContainerStep, } from '@documenso/ui/primitives/document-flow/document-flow-root'; -import { ShowFieldItem } from '@documenso/ui/primitives/document-flow/show-field-item'; import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { Form, @@ -97,14 +100,14 @@ export const DirectTemplateConfigureForm = ({ - {isDocumentPdfLoaded && - directTemplateRecipient.fields.map((field, index) => ( - - ))} + {isDocumentPdfLoaded && ( + + )}
)} -
+
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => { const itemValue = item.value || `empty-value-${item.id}`; return (
handleCheckboxChange(item.value, item.id)} /> -
@@ -277,7 +277,7 @@ export const DocumentSigningCheckboxField = ({ )} {field.inserted && ( -
+
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => { const itemValue = item.value || `empty-value-${item.id}`; @@ -290,7 +290,7 @@ export const DocumentSigningCheckboxField = ({ disabled={isLoading} onCheckedChange={() => void handleCheckboxOptionClick(item)} /> -
diff --git a/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx index 09a816b5b..97763c87e 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-date-field.tsx @@ -151,12 +151,10 @@ export const DocumentSigningDateField = ({

diff --git a/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx index a7ebc1dbe..9544abd1a 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx @@ -136,12 +136,10 @@ export const DocumentSigningEmailField = ({

diff --git a/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx index 7c0246c97..785edc625 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx @@ -182,12 +182,10 @@ export const DocumentSigningNameField = ({

diff --git a/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx index 307225778..0499c05fb 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx @@ -272,12 +272,10 @@ export const DocumentSigningNumberField = ({

diff --git a/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx b/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx index ddb9dc555..5fc86a973 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-page-view.tsx @@ -19,6 +19,7 @@ import { import type { CompletedField } from '@documenso/lib/types/fields'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; +import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; @@ -36,7 +37,6 @@ import { DocumentSigningRadioField } from '~/components/general/document-signing import { DocumentSigningRejectDialog } from '~/components/general/document-signing/document-signing-reject-dialog'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field'; -import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields'; import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider'; diff --git a/apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx index b518c7bb3..3d9344c06 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-radio-field.tsx @@ -157,17 +157,20 @@ export const DocumentSigningRadioField = ({ )} {!field.inserted && ( - handleSelectItem(value)} className="z-10"> + handleSelectItem(value)} + className="z-10 my-0.5 gap-y-1" + > {values?.map((item, index) => (

-
@@ -176,7 +179,7 @@ export const DocumentSigningRadioField = ({ )} {field.inserted && ( - + {values?.map((item, index) => (
-
diff --git a/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx index 074628cc4..da8a6eac6 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-text-field.tsx @@ -277,12 +277,11 @@ export const DocumentSigningTextField = ({

@@ -304,11 +303,9 @@ export const DocumentSigningTextField = ({ id="custom-text" placeholder={parsedFieldMeta?.placeholder ?? _(msg`Enter your text here`)} className={cn('mt-2 w-full rounded-md', { - 'border-2 border-red-300 ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200': + 'border-2 border-red-300 text-left ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200': userInputHasErrors, - 'text-left': parsedFieldMeta?.textAlign === 'left', - 'text-center': - !parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center', + 'text-center': parsedFieldMeta?.textAlign === 'center', 'text-right': parsedFieldMeta?.textAlign === 'right', })} value={localText} diff --git a/apps/remix/app/components/general/document/document-read-only-fields.tsx b/apps/remix/app/components/general/document/document-read-only-fields.tsx deleted file mode 100644 index 6970a88be..000000000 --- a/apps/remix/app/components/general/document/document-read-only-fields.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useState } from 'react'; - -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import type { DocumentMeta } from '@prisma/client'; -import { FieldType, SigningStatus } from '@prisma/client'; -import { Clock, EyeOffIcon } from 'lucide-react'; -import { P, match } from 'ts-pattern'; - -import { - DEFAULT_DOCUMENT_DATE_FORMAT, - convertToLocalSystemFormat, -} from '@documenso/lib/constants/date-formats'; -import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; -import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; -import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document'; -import { parseMessageDescriptor } from '@documenso/lib/utils/i18n'; -import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; -import { FieldRootContainer } from '@documenso/ui/components/field/field'; -import { SignatureIcon } from '@documenso/ui/icons/signature'; -import { cn } from '@documenso/ui/lib/utils'; -import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; -import { Badge } from '@documenso/ui/primitives/badge'; -import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types'; -import { ElementVisible } from '@documenso/ui/primitives/element-visible'; -import { PopoverHover } from '@documenso/ui/primitives/popover'; - -export type DocumentReadOnlyFieldsProps = { - fields: DocumentField[]; - documentMeta?: DocumentMeta; - showFieldStatus?: boolean; -}; - -export const DocumentReadOnlyFields = ({ - documentMeta, - fields, - showFieldStatus = true, -}: DocumentReadOnlyFieldsProps) => { - const { _ } = useLingui(); - - const [hiddenFieldIds, setHiddenFieldIds] = useState>({}); - - const handleHideField = (fieldId: string) => { - setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true })); - }; - - return ( - - {fields.map( - (field) => - !hiddenFieldIds[field.secondaryId] && ( - -

- - - {extractInitials(field.recipient.name || field.recipient.email)} - - - } - contentProps={{ - className: 'relative flex w-fit flex-col p-4 text-sm', - }} - > - {showFieldStatus && ( - - {field.recipient.signingStatus === SigningStatus.SIGNED ? ( - <> - - Signed - - ) : ( - <> - - Pending - - )} - - )} - -

- {parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field -

- -

- {field.recipient.name - ? `${field.recipient.name} (${field.recipient.email})` - : field.recipient.email}{' '} -

- - -
-
- -
- {field.recipient.signingStatus === SigningStatus.SIGNED && - match(field) - .with({ type: FieldType.SIGNATURE }, (field) => - field.signature?.signatureImageAsBase64 ? ( - Signature - ) : ( -

- {field.signature?.typedSignature} -

- ), - ) - .with( - { - type: P.union( - FieldType.NAME, - FieldType.INITIALS, - FieldType.EMAIL, - FieldType.NUMBER, - FieldType.RADIO, - FieldType.CHECKBOX, - FieldType.DROPDOWN, - ), - }, - () => field.customText, - ) - .with({ type: FieldType.TEXT }, () => field.customText.substring(0, 20) + '...') - .with({ type: FieldType.DATE }, () => - convertToLocalSystemFormat( - field.customText, - documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, - documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, - ), - ) - .with({ type: FieldType.FREE_SIGNATURE }, () => null) - .exhaustive()} - - {field.recipient.signingStatus === SigningStatus.NOT_SIGNED && ( -

- {parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} -

- )} -
- - ), - )} - - ); -}; diff --git a/apps/remix/app/routes/_authenticated+/documents.$id._index.tsx b/apps/remix/app/routes/_authenticated+/documents.$id._index.tsx index fd3c8c241..a0097ed03 100644 --- a/apps/remix/app/routes/_authenticated+/documents.$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/documents.$id._index.tsx @@ -13,6 +13,7 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; import { Badge } from '@documenso/ui/primitives/badge'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent } from '@documenso/ui/primitives/card'; @@ -24,7 +25,6 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information'; import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity'; import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients'; -import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields'; import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog'; import { DocumentStatus as DocumentStatusComponent, @@ -200,8 +200,12 @@ export default function DocumentPage() { - {document.status === DocumentStatus.PENDING && ( - + {document.status !== DocumentStatus.COMPLETED && ( + )}
diff --git a/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx b/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx index b8fc390b6..318cfdacb 100644 --- a/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx @@ -7,6 +7,7 @@ import { getSession } from '@documenso/auth/server/lib/utils/get-session'; import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; @@ -14,7 +15,6 @@ import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog'; import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper'; import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog'; -import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields'; import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge'; import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table'; import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information'; @@ -151,6 +151,7 @@ export default function TemplatePage() { diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 4d4d2dea1..91a6ff986 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -17,7 +17,6 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { getFileServerSide } from '../../universal/upload/get-file.server'; import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers'; -import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf'; import { flattenAnnotations } from '../pdf/flatten-annotations'; import { flattenForm } from '../pdf/flatten-form'; import { insertFieldInPDF } from '../pdf/insert-field-in-pdf'; @@ -104,13 +103,14 @@ export const sealDocument = async ({ // !: Need to write the fields onto the document as a hard copy const pdfData = await getFileServerSide(documentData); - const certificateData = - (document.team?.teamGlobalSettings?.includeSigningCertificate ?? true) - ? await getCertificatePdf({ - documentId, - language: document.documentMeta?.language, - }).catch(() => null) - : null; + // debugging........ + // const certificateData = + // (document.team?.teamGlobalSettings?.includeSigningCertificate ?? true) + // ? await getCertificatePdf({ + // documentId, + // language: document.documentMeta?.language, + // }).catch(() => null) + // : null; const doc = await PDFDocument.load(pdfData); @@ -119,15 +119,15 @@ export const sealDocument = async ({ flattenForm(doc); flattenAnnotations(doc); - if (certificateData) { - const certificate = await PDFDocument.load(certificateData); + // if (certificateData) { + // const certificate = await PDFDocument.load(certificateData); - const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices()); + // const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices()); - certificatePages.forEach((page) => { - doc.addPage(page); - }); - } + // certificatePages.forEach((page) => { + // doc.addPage(page); + // }); + // } for (const field of fields) { await insertFieldInPDF(doc, field); diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf.ts b/packages/lib/server-only/pdf/insert-field-in-pdf.ts index 403c42559..33970facc 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf.ts @@ -36,7 +36,8 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu const isSignatureField = isSignatureFieldType(field.type); const isDebugMode = // eslint-disable-next-line turbo/no-undeclared-env-vars - process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true'; + false; // todo + // true || process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true'; pdf.registerFontkit(fontkit); @@ -227,8 +228,13 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu const selected: string[] = fromCheckboxValue(field.customText); + const topPadding = 13; + const leftCheckboxPadding = 6; + const leftCheckboxLabelPadding = 12; + const checkboxSpaceY = 13; + for (const [index, item] of (values ?? []).entries()) { - const offsetY = index * 16; + const offsetY = index * checkboxSpaceY + topPadding; const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`); @@ -237,7 +243,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu } page.drawText(item.value.includes('empty-value-') ? '' : item.value, { - x: fieldX + 16, + x: fieldX + leftCheckboxPadding + leftCheckboxLabelPadding, y: pageHeight - (fieldY + offsetY), size: 12, font, @@ -245,7 +251,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu }); checkbox.addToPage(page, { - x: fieldX, + x: fieldX + leftCheckboxPadding, y: pageHeight - (fieldY + offsetY), height: 8, width: 8, @@ -268,21 +274,28 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu const selected = field.customText.split(','); + const topPadding = 13; + const leftRadioPadding = 6; + const leftRadioLabelPadding = 12; + const radioSpaceY = 13; + for (const [index, item] of (values ?? []).entries()) { - const offsetY = index * 16; + const offsetY = index * radioSpaceY + topPadding; const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`); + // Draw label. page.drawText(item.value.includes('empty-value-') ? '' : item.value, { - x: fieldX + 16, + x: fieldX + leftRadioPadding + leftRadioLabelPadding, y: pageHeight - (fieldY + offsetY), size: 12, font, rotate: degrees(pageRotationInDegrees), }); + // Draw radio button. radio.addOptionToPage(item.value, page, { - x: fieldX, + x: fieldX + leftRadioPadding, y: pageHeight - (fieldY + offsetY), height: 8, width: 8, @@ -308,7 +321,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu const meta = Parser ? Parser.safeParse(field.fieldMeta) : null; const customFontSize = meta?.success && meta.data.fontSize ? meta.data.fontSize : null; - const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'center'; + const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'left'; // ??? const longestLineInTextForWidth = field.customText .split('\n') .sort((a, b) => b.length - a.length)[0]; @@ -325,41 +338,69 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize); // Add padding similar to web display (roughly 0.5rem equivalent in PDF units) - const padding = 8; // PDF points, roughly equivalent to 0.5rem + const padding = 8; // Todo: Play around with this. // Calculate X position based on text alignment with padding let textX = fieldX + padding; // Left alignment starts after padding + if (textAlign === 'center') { textX = fieldX + (fieldWidth - textWidth) / 2; // Center alignment ignores padding } else if (textAlign === 'right') { textX = fieldX + fieldWidth - textWidth - padding; // Right alignment respects right padding } - let textY = fieldY + (fieldHeight - textHeight) / 2; - // Invert the Y axis since PDFs use a bottom-left coordinate system - textY = pageHeight - textY - textHeight; + let textFieldBoxY = pageHeight - fieldY - fieldHeight; + let textFieldBoxX = textX; + + const textField = pdf.getForm().createTextField(`text.${field.secondaryId}`); if (pageRotationInDegrees !== 0) { const adjustedPosition = adjustPositionForRotation( pageWidth, pageHeight, - textX, - textY, + textFieldBoxX, + textFieldBoxY, pageRotationInDegrees, ); - textX = adjustedPosition.xPos; - textY = adjustedPosition.yPos; + textFieldBoxX = adjustedPosition.xPos; + textFieldBoxY = adjustedPosition.yPos; } - page.drawText(field.customText, { - x: textX, - y: textY, - size: fontSize, - font, + if (isDebugMode) { + page.drawRectangle({ + x: textFieldBoxX, + y: textFieldBoxY, + width: textWidth, + height: textHeight, + borderColor: rgb(1, 0, 0), + borderWidth: 1, + rotate: degrees(pageRotationInDegrees), + }); + } + + // Set the position and size of the text field + textField.addToPage(page, { + x: textFieldBoxX, + y: textFieldBoxY, + width: fieldWidth, + height: fieldHeight, + borderWidth: 0, // Hide border. + borderColor: rgb(1, 1, 1), // Hide border. + backgroundColor: undefined, // Makes transparent so background doesn't cover other text. rotate: degrees(pageRotationInDegrees), + + // Draw debug box if debug mode is enabled. + ...(isDebugMode && { + borderColor: rgb(1, 0, 0), + borderWidth: 1, + }), }); + + // Set properties for the text field + textField.setFontSize(fontSize); + textField.setText(field.customText); }); return pdf; diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs index 437ad6497..e10e75daf 100644 --- a/packages/tailwind-config/index.cjs +++ b/packages/tailwind-config/index.cjs @@ -103,6 +103,14 @@ module.exports = { 900: '#364772', 950: '#252d46', }, + signer: { + green: 'hsl(var(--signer-green))', + blue: 'hsl(var(--signer-blue))', + purple: 'hsl(var(--signer-purple))', + orange: 'hsl(var(--signer-orange))', + yellow: 'hsl(var(--signer-yellow))', + pink: 'hsl(var(--signer-pink))', + }, }, backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', diff --git a/packages/ui/components/document/document-read-only-fields.tsx b/packages/ui/components/document/document-read-only-fields.tsx new file mode 100644 index 000000000..80eae459d --- /dev/null +++ b/packages/ui/components/document/document-read-only-fields.tsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from 'react'; + +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type { DocumentMeta, Field, Recipient } from '@prisma/client'; +import { SigningStatus } from '@prisma/client'; +import { Clock, EyeOffIcon } from 'lucide-react'; + +import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; +import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document'; +import { parseMessageDescriptor } from '@documenso/lib/utils/i18n'; +import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; +import { FieldRootContainer } from '@documenso/ui/components/field/field'; +import { SignatureIcon } from '@documenso/ui/icons/signature'; +import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; +import { Badge } from '@documenso/ui/primitives/badge'; +import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types'; +import { ElementVisible } from '@documenso/ui/primitives/element-visible'; +import { PopoverHover } from '@documenso/ui/primitives/popover'; + +import { FieldContent } from '../../primitives/document-flow/field-content'; + +export type DocumentReadOnlyFieldsProps = { + fields: DocumentField[]; + documentMeta?: DocumentMeta; + showFieldStatus?: boolean; + + /** + * Whether to show the recipient tooltip. + * + * @default false + */ + showRecipientTooltip?: boolean; +}; + +export const mapFieldsWithRecipients = ( + fields: Field[], + recipients: Recipient[], +): DocumentField[] => { + return fields.map((field) => { + const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || { + name: 'Unknown', + email: 'Unknown', + signingStatus: SigningStatus.NOT_SIGNED, + }; + + return { ...field, recipient, signature: null }; + }); +}; + +export const DocumentReadOnlyFields = ({ + documentMeta, + fields, + showFieldStatus = true, + showRecipientTooltip = false, +}: DocumentReadOnlyFieldsProps) => { + const { _ } = useLingui(); + + const [hiddenFieldIds, setHiddenFieldIds] = useState>({}); + + const handleHideField = (fieldId: string) => { + setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true })); + }; + + const isMounted = useIsMounted(); + + useEffect(() => { + console.log(isMounted); + }, [isMounted]); + + if (!isMounted) { + return null; + } + + return ( + + {fields.map( + (field) => + !hiddenFieldIds[field.secondaryId] && ( + + {showRecipientTooltip && ( +
+ + + {extractInitials(field.recipient.name || field.recipient.email)} + + + } + contentProps={{ + className: 'relative flex w-fit flex-col p-4 text-sm', + }} + > + {showFieldStatus && ( + + {field.recipient.signingStatus === SigningStatus.SIGNED ? ( + <> + + Signed + + ) : ( + <> + + Pending + + )} + + )} + +

+ + {parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field + +

+ +

+ {field.recipient.name + ? `${field.recipient.name} (${field.recipient.email})` + : field.recipient.email}{' '} +

+ + +
+
+ )} + + +
+ ), + )} +
+ ); +}; diff --git a/packages/ui/components/field/field.tsx b/packages/ui/components/field/field.tsx index bc648ccbf..3c3a90ce4 100644 --- a/packages/ui/components/field/field.tsx +++ b/packages/ui/components/field/field.tsx @@ -1,14 +1,12 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import type { Field } from '@prisma/client'; import { createPortal } from 'react-dom'; import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords'; -import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; -import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; +import { useSignerColors } from '../../lib/signer-colors'; import { cn } from '../../lib/utils'; -import { Card, CardContent } from '../../primitives/card'; export type FieldRootContainerProps = { field: Field; @@ -19,53 +17,6 @@ export type FieldContainerPortalProps = { field: Field; className?: string; children: React.ReactNode; - cardClassName?: string; -}; - -const getCardClassNames = ( - field: Field, - parsedField: TFieldMetaSchema | null, - isValidating: boolean, - checkBoxOrRadio: boolean, - cardClassName?: string, -) => { - const baseClasses = - 'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all'; - - const insertedClasses = - 'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none'; - const nonRequiredClasses = - 'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2'; - const validatingClasses = 'border-orange-300 ring-1 ring-orange-300'; - const requiredClasses = - 'border-red-500 shadow-none ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 hover:text-red-500'; - const requiredCheckboxRadioClasses = 'border-dashed border-red-500'; - - if (checkBoxOrRadio) { - return cn( - { - [insertedClasses]: field.inserted, - 'ring-offset-yellow-200 border-dashed border-yellow-300 ring-2 ring-yellow-200 ring-offset-2 dark:shadow-none': - !field.inserted && !parsedField?.required, - 'shadow-none': !field.inserted, - [validatingClasses]: !field.inserted && isValidating, - [requiredCheckboxRadioClasses]: !field.inserted && parsedField?.required, - }, - cardClassName, - ); - } - - return cn( - baseClasses, - { - [insertedClasses]: field.inserted, - [nonRequiredClasses]: !field.inserted && !parsedField?.required, - 'shadow-none': !field.inserted && checkBoxOrRadio, - [validatingClasses]: !field.inserted && isValidating, - [requiredClasses]: !field.inserted && parsedField?.required && !checkBoxOrRadio, - }, - cardClassName, - ); }; export function FieldContainerPortal({ @@ -76,13 +27,10 @@ export function FieldContainerPortal({ const coords = useFieldPageCoords(field); const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO'; - const isFieldSigned = field.inserted; const style = { top: `${coords.y}px`, left: `${coords.x}px`, - // height: `${coords.height}px`, - // width: `${coords.width}px`, ...(!isCheckboxOrRadioField && { height: `${coords.height}px`, width: `${coords.width}px`, @@ -97,10 +45,12 @@ export function FieldContainerPortal({ ); } -export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) { +export function FieldRootContainer({ field, children }: FieldContainerPortalProps) { const [isValidating, setIsValidating] = useState(false); const ref = React.useRef(null); + const signerStyles = useSignerColors(field.recipientId); + useEffect(() => { if (!ref.current) { return; @@ -121,33 +71,36 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont }; }, []); - const parsedField = useMemo( - () => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null), - [field.fieldMeta], - ); - const isCheckboxOrRadio = useMemo( - () => parsedField?.type === 'checkbox' || parsedField?.type === 'radio', - [parsedField], - ); + // // todo: remove + // const parsedField = useMemo( + // () => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null), + // [field.fieldMeta], + // ); - const cardClassNames = useMemo( - () => getCardClassNames(field, parsedField, isValidating, isCheckboxOrRadio, cardClassName), - [field, parsedField, isValidating, isCheckboxOrRadio, cardClassName], - ); + // // todo: remove + // const isCheckboxOrRadio = useMemo( + // () => parsedField?.type === 'checkbox' || parsedField?.type === 'radio', + // [parsedField], + // ); return ( - - - {children} - - + {children} +
); } diff --git a/packages/ui/lib/signer-colors.ts b/packages/ui/lib/signer-colors.ts index 862b75e58..43039d8dd 100644 --- a/packages/ui/lib/signer-colors.ts +++ b/packages/ui/lib/signer-colors.ts @@ -2,16 +2,14 @@ // !: therefore doing this at runtime is not possible without whitelisting a set of classnames. // !: // !: This will later be improved as we move to a CSS variable approach and rotate the lightness + // !: values of the declared variable to do all the background, border and shadow styles. export const SIGNER_COLOR_STYLES = { green: { default: { - background: 'bg-[hsl(var(--signer-green))]', - base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-green)/10%),0_0_0_2px_hsl(var(--signer-green)/60%),0_0_0_0.5px_hsl(var(--signer-green))]', - fieldItem: - 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-green))]/10 hover:to-[hsl(var(--signer-green))]/10', - fieldItemInitials: - 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-green))]', + base: 'ring-signer-green hover:bg-signer-green/30', + fieldItem: 'group/field-item rounded-sm', + fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-green))]', comboxBoxItem: 'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]', }, @@ -19,12 +17,9 @@ export const SIGNER_COLOR_STYLES = { blue: { default: { - background: 'bg-[hsl(var(--signer-blue))]', - base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-blue)/10%),0_0_0_2px_hsl(var(--signer-blue)/60%),0_0_0_0.5px_hsl(var(--signer-blue))]', - fieldItem: - 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-blue))]/10 hover:to-[hsl(var(--signer-blue))]/10', - fieldItemInitials: - 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-blue))]', + base: 'ring-signer-blue hover:bg-signer-blue/30', + fieldItem: 'group/field-item rounded-sm', + fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-blue))]', comboxBoxItem: 'hover:bg-[hsl(var(--signer-blue)/15%)] active:bg-[hsl(var(--signer-blue)/15%)]', }, @@ -32,12 +27,9 @@ export const SIGNER_COLOR_STYLES = { purple: { default: { - background: 'bg-[hsl(var(--signer-purple))]', - base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-purple)/10%),0_0_0_2px_hsl(var(--signer-purple)/60%),0_0_0_0.5px_hsl(var(--signer-purple))]', - fieldItem: - 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-purple))]/10 hover:to-[hsl(var(--signer-purple))]/10', - fieldItemInitials: - 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-purple))]', + base: 'ring-signer-purple hover:bg-signer-purple/30', + fieldItem: 'group/field-item rounded-sm', + fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-purple))]', comboxBoxItem: 'hover:bg-[hsl(var(--signer-purple)/15%)] active:bg-[hsl(var(--signer-purple)/15%)]', }, @@ -45,12 +37,9 @@ export const SIGNER_COLOR_STYLES = { orange: { default: { - background: 'bg-[hsl(var(--signer-orange))]', - base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-orange)/10%),0_0_0_2px_hsl(var(--signer-orange)/60%),0_0_0_0.5px_hsl(var(--signer-orange))]', - fieldItem: - 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-orange))]/10 hover:to-[hsl(var(--signer-orange))]/10', - fieldItemInitials: - 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-orange))]', + base: 'ring-signer-orange hover:bg-signer-orange/30', + fieldItem: 'group/field-item rounded-sm', + fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-orange))]', comboxBoxItem: 'hover:bg-[hsl(var(--signer-orange)/15%)] active:bg-[hsl(var(--signer-orange)/15%)]', }, @@ -58,12 +47,9 @@ export const SIGNER_COLOR_STYLES = { yellow: { default: { - background: 'bg-[hsl(var(--signer-yellow))]', - base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-yellow)/10%),0_0_0_2px_hsl(var(--signer-yellow)/60%),0_0_0_0.5px_hsl(var(--signer-yellow))]', - fieldItem: - 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-yellow))]/10 hover:to-[hsl(var(--signer-yellow))]/10', - fieldItemInitials: - 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-yellow))]', + base: 'ring-signer-yellow hover:bg-signer-yellow/30', + fieldItem: 'group/field-item rounded-sm', + fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-yellow))]', comboxBoxItem: 'hover:bg-[hsl(var(--signer-yellow)/15%)] active:bg-[hsl(var(--signer-yellow)/15%)]', }, @@ -71,12 +57,9 @@ export const SIGNER_COLOR_STYLES = { pink: { default: { - background: 'bg-[hsl(var(--signer-pink))]', - base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-pink)/10%),0_0_0_2px_hsl(var(--signer-pink)/60%),0_0_0_0.5px_hsl(var(--signer-pink))]', - fieldItem: - 'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-pink))]/10 hover:to-[hsl(var(--signer-pink))]/10', - fieldItemInitials: - 'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-pink))]', + base: 'ring-signer-pink hover:bg-signer-pink/30', + fieldItem: 'group/field-item rounded-sm', + fieldItemInitials: 'group-hover/field-item:bg-[hsl(var(--signer-pink))]', comboxBoxItem: 'hover:bg-[hsl(var(--signer-pink)/15%)] active:bg-[hsl(var(--signer-pink)/15%)]', }, diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 39139e29b..dbe40b453 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -88,7 +88,6 @@ export type FieldFormType = { export type AddFieldsFormProps = { documentFlow: DocumentFlowStep; - hideRecipients?: boolean; recipients: Recipient[]; fields: Field[]; onSubmit: (_data: TAddFieldsFormSchema) => void; @@ -100,7 +99,6 @@ export type AddFieldsFormProps = { export const AddFieldsFormPartial = ({ documentFlow, - hideRecipients = false, recipients, fields, onSubmit, @@ -657,7 +655,6 @@ export const AddFieldsFormPartial = ({ setCurrentField(field); handleAdvancedSettings(); }} - hideRecipients={hideRecipients} hasErrors={!!hasFieldError} active={activeFieldId === field.formId} onFieldActivate={() => setActiveFieldId(field.formId)} @@ -666,125 +663,123 @@ export const AddFieldsFormPartial = ({ ); })} - {!hideRecipients && ( - - - - + + + - - - + + + - - - No recipient matching this description was found. - - + + + No recipient matching this description was found. + + - {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => ( - -
- {_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} + {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => ( + +
+ {_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} +
+ + {roleRecipients.length === 0 && ( +
+ No recipients with this role
+ )} - {roleRecipients.length === 0 && ( -
( + r.id === recipient.id), + 0, + ), + ).default.comboxBoxItem, + { + 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT, + }, + )} + onSelect={() => { + setSelectedSigner(recipient); + setShowRecipientsSelector(false); + }} + > + - No recipients with this role -
- )} - - {roleRecipients.map((recipient) => ( - r.id === recipient.id), - 0, - ), - ).default.comboxBoxItem, - { - 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT, - }, + {recipient.name && ( + + {recipient.name} ({recipient.email}) + )} - onSelect={() => { - setSelectedSigner(recipient); - setShowRecipientsSelector(false); - }} - > - - {recipient.name && ( - - {recipient.name} ({recipient.email}) - - )} - {!recipient.name && ( - {recipient.email} - )} - + {!recipient.name && ( + {recipient.email} + )} + -
- {recipient.sendStatus !== SendStatus.SENT ? ( - - ) : ( - - - - +
+ {recipient.sendStatus !== SendStatus.SENT ? ( + + ) : ( + + + + - - - This document has already been sent to this recipient. You - can no longer edit this recipient. - - - - )} -
- - ))} - - ))} - - - - )} + + + This document has already been sent to this recipient. You can + no longer edit this recipient. + + +
+ )} +
+
+ ))} +
+ ))} + + + - {isDocumentPdfLoaded && - fields.map((field, index) => ( - - ))} + {isDocumentPdfLoaded && ( + + )}
- {isDocumentPdfLoaded && - fields.map((field, index) => ( - - ))} + {isDocumentPdfLoaded && ( + + )} diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index aec6decee..c39b03180 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -16,6 +16,10 @@ import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { CopyTextButton } from '../../components/common/copy-text-button'; import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes'; +import { + DocumentReadOnlyFields, + mapFieldsWithRecipients, +} from '../../components/document/document-read-only-fields'; import { AvatarWithText } from '../avatar'; import { FormErrorMessage } from '../form/form-error-message'; import { Input } from '../input'; @@ -31,7 +35,6 @@ import { DocumentFlowFormContainerHeader, DocumentFlowFormContainerStep, } from './document-flow-root'; -import { ShowFieldItem } from './show-field-item'; import type { DocumentFlowStep } from './types'; export type AddSubjectFormProps = { @@ -101,10 +104,9 @@ export const AddSubjectFormPartial = ({ />
- {isDocumentPdfLoaded && - fields.map((field, index) => ( - - ))} + {isDocumentPdfLoaded && ( + + )} diff --git a/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx b/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx deleted file mode 100644 index d0ff41e5f..000000000 --- a/packages/ui/primitives/document-flow/advanced-fields/checkbox.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta'; -import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta'; -import { Checkbox } from '@documenso/ui/primitives/checkbox'; -import { Label } from '@documenso/ui/primitives/label'; - -import { FieldIcon } from '../field-icon'; -import type { TDocumentFlowFormSchema } from '../types'; - -type Field = TDocumentFlowFormSchema['fields'][0]; - -export type CheckboxFieldProps = { - field: Field; -}; - -export const CheckboxField = ({ field }: CheckboxFieldProps) => { - let parsedFieldMeta: TCheckboxFieldMeta | undefined = undefined; - - if (field.fieldMeta) { - parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta); - } - - if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) { - return ; - } - - return ( -
- {!parsedFieldMeta?.values ? ( - - ) : ( - parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => ( -
- - -
- )) - )} -
- ); -}; diff --git a/packages/ui/primitives/document-flow/advanced-fields/radio.tsx b/packages/ui/primitives/document-flow/advanced-fields/radio.tsx deleted file mode 100644 index 28af5a10f..000000000 --- a/packages/ui/primitives/document-flow/advanced-fields/radio.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta'; -import type { TRadioFieldMeta } from '@documenso/lib/types/field-meta'; -import { Label } from '@documenso/ui/primitives/label'; -import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group'; - -import { FieldIcon } from '../field-icon'; -import type { TDocumentFlowFormSchema } from '../types'; - -type Field = TDocumentFlowFormSchema['fields'][0]; - -export type RadioFieldProps = { - field: Field; -}; - -export const RadioField = ({ field }: RadioFieldProps) => { - let parsedFieldMeta: TRadioFieldMeta | undefined = undefined; - - if (field.fieldMeta) { - parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta); - } - - if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) { - return ; - } - - return ( -
- {!parsedFieldMeta?.values ? ( - - ) : ( - - {parsedFieldMeta.values?.map((item, index) => ( -
- - -
- ))} -
- )} -
- ); -}; diff --git a/packages/ui/primitives/document-flow/field-content.tsx b/packages/ui/primitives/document-flow/field-content.tsx new file mode 100644 index 000000000..02aa287a5 --- /dev/null +++ b/packages/ui/primitives/document-flow/field-content.tsx @@ -0,0 +1,166 @@ +import { useLingui } from '@lingui/react'; +import type { DocumentMeta, Field, Signature } from '@prisma/client'; +import { FieldType } from '@prisma/client'; +import { ChevronDown } from 'lucide-react'; + +import { + DEFAULT_DOCUMENT_DATE_FORMAT, + convertToLocalSystemFormat, +} from '@documenso/lib/constants/date-formats'; +import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox'; + +import { cn } from '../../lib/utils'; +import { Checkbox } from '../checkbox'; +import { Label } from '../label'; +import { RadioGroup, RadioGroupItem } from '../radio-group'; +import { FRIENDLY_FIELD_TYPE } from './types'; + +type FieldIconProps = { + field: Field & { signature?: Signature }; + documentMeta?: DocumentMeta; +}; + +/** + * Renders the content inside field containers prior to sealing. + */ +export const FieldContent = ({ field, documentMeta }: FieldIconProps) => { + const { _ } = useLingui(); + + const { type, fieldMeta } = field; + + // Only render checkbox if values exist, otherwise render the empty checkbox field content. + if ( + field.type === FieldType.CHECKBOX && + field.fieldMeta?.type === 'checkbox' && + field.fieldMeta.values && + field.fieldMeta.values.length > 0 + ) { + let checkedValues: string[] = []; + + try { + checkedValues = fromCheckboxValue(field.customText); + } catch (err) { + // Do nothing. + + console.error(err); + } + + return ( +
+ {field.fieldMeta.values.map((item, index) => ( +
+ + + +
+ ))} +
+ ); + } + + // Only render radio if values exist, otherwise render the empty radio field content. + if ( + field.type === FieldType.RADIO && + field.fieldMeta?.type === 'radio' && + field.fieldMeta.values && + field.fieldMeta.values.length > 0 + ) { + return ( +
+ + {field.fieldMeta.values.map((item, index) => ( +
+ + +
+ ))} +
+
+ ); + } + + if ( + field.type === FieldType.DROPDOWN && + field.fieldMeta?.type === 'dropdown' && + !field.inserted + ) { + return ( +
+

Select

+ +
+ ); + } + + if ( + field.type === FieldType.SIGNATURE && + field.signature?.signatureImageAsBase64 && + field.inserted + ) { + return ( + Signature + ); + } + + let textToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || ''; + + const isSignatureField = + field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE; + + // Trim default labels. + if (textToDisplay.length > 20) { + textToDisplay = textToDisplay.substring(0, 20) + '...'; + } + + if (field.inserted) { + if (field.customText) { + textToDisplay = field.customText; + } + + if (field.type === FieldType.DATE) { + textToDisplay = convertToLocalSystemFormat( + field.customText, + documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + ); + } + + if (isSignatureField && field.signature?.typedSignature) { + textToDisplay = field.signature.typedSignature; + } + } + + const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left'; + + return ( +
+ {textToDisplay} +
+ ); +}; diff --git a/packages/ui/primitives/document-flow/field-icon.tsx b/packages/ui/primitives/document-flow/field-icon.tsx deleted file mode 100644 index f1b9e412a..000000000 --- a/packages/ui/primitives/document-flow/field-icon.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Trans } from '@lingui/react/macro'; -import { FieldType } from '@prisma/client'; -import { - CalendarDays, - CheckSquare, - ChevronDown, - Contact, - Disc, - Hash, - Mail, - Type, - User, -} from 'lucide-react'; - -import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta'; - -import { cn } from '../../lib/utils'; - -type FieldIconProps = { - fieldMeta: FieldMetaType; - type: FieldType; -}; - -const fieldIcons = { - [FieldType.INITIALS]: { icon: Contact, label: 'Initials' }, - [FieldType.EMAIL]: { icon: Mail, label: 'Email' }, - [FieldType.NAME]: { icon: User, label: 'Name' }, - [FieldType.DATE]: { icon: CalendarDays, label: 'Date' }, - [FieldType.TEXT]: { icon: Type, label: 'Text' }, - [FieldType.NUMBER]: { icon: Hash, label: 'Number' }, - [FieldType.RADIO]: { icon: Disc, label: 'Radio' }, - [FieldType.CHECKBOX]: { icon: CheckSquare, label: 'Checkbox' }, - [FieldType.DROPDOWN]: { icon: ChevronDown, label: 'Select' }, -}; - -export const FieldIcon = ({ fieldMeta, type }: FieldIconProps) => { - if (type === 'SIGNATURE' || type === 'FREE_SIGNATURE') { - return ( -
- Signature -
- ); - } else { - const Icon = fieldIcons[type]?.icon; - let label; - - if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) { - if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) { - label = - fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text; - } else if (fieldMeta.label) { - label = - fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label; - } else { - label = fieldIcons[type]?.label; - } - } else { - label = fieldIcons[type]?.label; - } - - return ( -
- {' '} - {label} -
- ); - } -}; diff --git a/packages/ui/primitives/document-flow/field-item.tsx b/packages/ui/primitives/document-flow/field-item.tsx index f96fd3aee..e8e9b6ebe 100644 --- a/packages/ui/primitives/document-flow/field-item.tsx +++ b/packages/ui/primitives/document-flow/field-item.tsx @@ -4,7 +4,6 @@ import { FieldType } from '@prisma/client'; import { CopyPlus, Settings2, Trash } from 'lucide-react'; import { createPortal } from 'react-dom'; import { Rnd } from 'react-rnd'; -import { match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; @@ -12,9 +11,7 @@ import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field- import { useSignerColors } from '../../lib/signer-colors'; import { cn } from '../../lib/utils'; -import { CheckboxField } from './advanced-fields/checkbox'; -import { RadioField } from './advanced-fields/radio'; -import { FieldIcon } from './field-icon'; +import { FieldContent } from './field-content'; import type { TDocumentFlowFormSchema } from './types'; type Field = TDocumentFlowFormSchema['fields'][0]; @@ -35,13 +32,15 @@ export type FieldItemProps = { onFocus?: () => void; onBlur?: () => void; recipientIndex?: number; - hideRecipients?: boolean; hasErrors?: boolean; active?: boolean; onFieldActivate?: () => void; onFieldDeactivate?: () => void; }; +/** + * The item when editing fields?? + */ export const FieldItem = ({ field, passive, @@ -58,7 +57,6 @@ export const FieldItem = ({ onBlur, onAdvancedSettings, recipientIndex = 0, - hideRecipients = false, hasErrors, active, onFieldActivate, @@ -260,11 +258,11 @@ export const FieldItem = ({
- {match(field.type) - .with('CHECKBOX', () => ) - .with('RADIO', () => ) - .otherwise(() => ( - - ))} + - {!hideRecipients && ( -
-
- {(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') + - (field.signerEmail?.charAt(1)?.toUpperCase() ?? '')} -
+ {/* On hover, display recipient initials on side of field. */} +
+
+ {(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') + + (field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
- )} +
{!disabled && settingsActive && ( -
+
{advancedField && ( - + + + - - - + + + - - - No recipient matching this description was found. - - + + + No recipient matching this description was found. + + - {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => ( - -
- {_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} + {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => ( + +
+ {_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} +
+ + {roleRecipients.length === 0 && ( +
+ No recipients with this role
+ )} - {roleRecipients.length === 0 && ( -
( + r.id === recipient.id), + 0, + ), + ).default.comboxBoxItem, + )} + onSelect={() => { + setSelectedSigner(recipient); + setShowRecipientsSelector(false); + }} + > + - No recipients with this role -
- )} - - {roleRecipients.map((recipient) => ( - r.id === recipient.id), - 0, - ), - ).default.comboxBoxItem, + {recipient.name && ( + + {recipient.name} ({recipient.email}) + )} - onSelect={() => { - setSelectedSigner(recipient); - setShowRecipientsSelector(false); - }} - > - - {recipient.name && ( - - {recipient.name} ({recipient.email}) - - )} - {!recipient.name && ( - {recipient.email} - )} - - - ))} -
- ))} - - - - )} + {!recipient.name && ( + {recipient.email} + )} + + + ))} + + ))} + + + - {isDocumentPdfLoaded && - fields.map((field, index) => ( - - ))} + {isDocumentPdfLoaded && ( + + )} @@ -447,6 +449,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ ref={provided.innerRef} className="flex w-full flex-col gap-y-2" > + {/* todo */} {signers.map((signer, index) => ( - {isDocumentPdfLoaded && - fields.map((field, index) => ( - - ))} + {isDocumentPdfLoaded && ( + + )}