diff --git a/.github/actions/cache-build/action.yml b/.github/actions/cache-build/action.yml deleted file mode 100644 index af89fedef..000000000 --- a/.github/actions/cache-build/action.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Cache production build binaries -description: 'Cache or restore if necessary' -inputs: - node_version: - required: false - default: v22.x -runs: - using: 'composite' - steps: - - name: Cache production build - uses: actions/cache@v3 - id: production-build-cache - with: - path: | - ${{ github.workspace }}/apps/web/.next - **/.turbo/** - **/dist/** - - key: prod-build-${{ github.run_id }}-${{ hashFiles('package-lock.json') }} - restore-keys: prod-build- - - - run: npm run build - shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6101b0180..3a7afc44f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,8 @@ jobs: - name: Copy env run: cp .env.example .env - - uses: ./.github/actions/cache-build + - name: Build app + run: npm run build build_docker: name: Build Docker Image diff --git a/.github/workflows/clean-cache.yml b/.github/workflows/clean-cache.yml deleted file mode 100644 index 2cb13f661..000000000 --- a/.github/workflows/clean-cache.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: cleanup caches by a branch -on: - pull_request: - types: - - closed - -jobs: - cleanup: - runs-on: ubuntu-latest - steps: - - name: Cleanup - run: | - gh extension install actions/gh-actions-cache - - echo "Fetching list of cache key" - cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) - - ## Setting this to not fail the workflow while deleting cache keys. - set +e - echo "Deleting caches..." - for cacheKey in $cacheKeysForPR - do - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm - done - echo "Done" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 88692396f..d74f30387 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -10,7 +10,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: actions: read contents: read @@ -30,7 +30,8 @@ jobs: - uses: ./.github/actions/node-install - - uses: ./.github/actions/cache-build + - name: Build app + run: npm run build - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9d935d83f..5a33362a9 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -28,7 +28,8 @@ jobs: - name: Seed the database run: npm run prisma:seed - - uses: ./.github/actions/cache-build + - name: Build app + run: npm run build - name: Run Playwright tests run: npm run ci diff --git a/.husky/commit-msg b/.husky/commit-msg index 6610d88b5..09fe18a70 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - npm run commitlint -- $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index 52007e38b..13254ace8 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,3 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - SCRIPT_DIR="$(readlink -f "$(dirname "$0")")" MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")" diff --git a/apps/documentation/pages/developers/embedding/_meta.json b/apps/documentation/pages/developers/embedding/_meta.json index 96806de3e..78d8b2503 100644 --- a/apps/documentation/pages/developers/embedding/_meta.json +++ b/apps/documentation/pages/developers/embedding/_meta.json @@ -6,5 +6,6 @@ "solid": "Solid Integration", "preact": "Preact Integration", "angular": "Angular Integration", - "css-variables": "CSS Variables" -} + "css-variables": "CSS Variables", + "authoring": "Authoring" +} \ No newline at end of file diff --git a/apps/documentation/pages/developers/embedding/authoring.mdx b/apps/documentation/pages/developers/embedding/authoring.mdx new file mode 100644 index 000000000..8d5c4d09f --- /dev/null +++ b/apps/documentation/pages/developers/embedding/authoring.mdx @@ -0,0 +1,167 @@ +--- +title: Authoring +description: Learn how to use embedded authoring to create documents and templates in your application +--- + +# Embedded Authoring + +In addition to embedding signing experiences, Documenso now supports embedded authoring, allowing you to integrate document and template creation directly within your application. + +## How Embedded Authoring Works + +The embedded authoring feature enables your users to create new documents without leaving your application. This process works through secure presign tokens that authenticate the embedding session and manage permissions. + +## Creating Documents with Embedded Authoring + +To implement document creation in your application, use the `EmbedCreateDocument` component from our SDK: + +```jsx +import { unstable_EmbedCreateDocument as EmbedCreateDocument } from '@documenso/embed-react'; + +const DocumentCreator = () => { + // You'll need to obtain a presign token using your API key + const presignToken = 'YOUR_PRESIGN_TOKEN'; + + return ( +
+ { + console.log('Document created with ID:', data.documentId); + console.log('External reference ID:', data.externalId); + }} + /> +
+ ); +}; +``` + +## Obtaining a Presign Token + +Before using the `EmbedCreateDocument` component, you'll need to obtain a presign token from your backend. This token authorizes the embedding session. + +You can create a presign token by making a request to: + +``` +POST /api/v2-beta/embedding/create-presign-token +``` + +This API endpoint requires authentication with your Documenso API key. The token has a default expiration of 1 hour, but you can customize this duration based on your security requirements. + +You can find more details on this request at our [API Documentation](https://openapi.documenso.com/reference#tag/embedding) + +## Configuration Options + +The `EmbedCreateDocument` component accepts several configuration options: + +| Option | Type | Description | +| ------------------ | ------- | ------------------------------------------------------------------ | +| `presignToken` | string | **Required**. The authentication token for the embedding session. | +| `externalId` | string | Optional reference ID from your system to link with the document. | +| `host` | string | Optional custom host URL. Defaults to `https://app.documenso.com`. | +| `css` | string | Optional custom CSS to style the embedded component. | +| `cssVars` | object | Optional CSS variables for colors, spacing, and more. | +| `darkModeDisabled` | boolean | Optional flag to disable dark mode. | +| `className` | string | Optional CSS class name for the iframe. | + +## Feature Toggles + +You can customize the authoring experience by enabling or disabling specific features: + +```jsx + +``` + +## Handling Document Creation Events + +The `onDocumentCreated` callback is triggered when a document is successfully created, providing both the document ID and your external reference ID: + +```jsx + { + // Navigate to a success page + navigate(`/documents/success?id=${data.documentId}`); + + // Or update your database with the document ID + updateOrderDocument(data.externalId, data.documentId); + }} +/> +``` + +## Styling the Embedded Component + +You can customize the appearance of the embedded component using standard CSS classes: + +```jsx + +``` + +## Complete Integration Example + +Here's a complete example of integrating document creation in a React application: + +```tsx +import { useState } from 'react'; + +import { unstable_EmbedCreateDocument as EmbedCreateDocument } from '@documenso/embed-react'; + +function DocumentCreator() { + // In a real application, you would fetch this token from your backend + // using your API key at /api/v2-beta/embedding/create-presign-token + const presignToken = 'YOUR_PRESIGN_TOKEN'; + const [documentId, setDocumentId] = useState(null); + + if (documentId) { + return ( +
+

Document Created Successfully!

+

Document ID: {documentId}

+ +
+ ); + } + + return ( +
+ { + setDocumentId(data.documentId); + }} + /> +
+ ); +} + +export default DocumentCreator; +``` + +With embedded authoring, your users can seamlessly create documents within your application, enhancing the overall user experience and streamlining document workflows. diff --git a/apps/documentation/pages/developers/embedding/index.mdx b/apps/documentation/pages/developers/embedding/index.mdx index 3a5535f89..7fd3d4193 100644 --- a/apps/documentation/pages/developers/embedding/index.mdx +++ b/apps/documentation/pages/developers/embedding/index.mdx @@ -169,6 +169,19 @@ Once you've obtained the appropriate tokens, you can integrate the signing exper If you're using **web components**, the integration process is slightly different. Keep in mind that web components are currently less tested but can still provide flexibility for general use cases. +## Embedded Authoring + +In addition to embedding signing experiences, Documenso now supports **embedded authoring**, allowing your users to create documents and templates directly within your application. + +With embedded authoring, you can: + +- Create new documents with custom fields +- Configure document properties and settings +- Set up recipients and signing workflows +- Customize the authoring experience + +For detailed implementation instructions and code examples, see our [Embedded Authoring](/developers/embedding/authoring) guide. + ## Related - [React Integration](/developers/embedding/react) @@ -178,3 +191,4 @@ If you're using **web components**, the integration process is slightly differen - [Preact Integration](/developers/embedding/preact) - [Angular Integration](/developers/embedding/angular) - [CSS Variables](/developers/embedding/css-variables) +- [Embedded Authoring](/developers/embedding/authoring) diff --git a/apps/documentation/pages/developers/public-api/reference.mdx b/apps/documentation/pages/developers/public-api/reference.mdx index 906b499fd..db0232797 100644 --- a/apps/documentation/pages/developers/public-api/reference.mdx +++ b/apps/documentation/pages/developers/public-api/reference.mdx @@ -532,3 +532,93 @@ Replace the `text` value with the corresponding field type: - For the `SELECT` field it should be `select`. (check this before merge) You must pass this property at all times, even if you don't need to set any other properties. If you don't, the endpoint will throw an error. + +## Pre-fill Fields On Document Creation + +The API allows you to pre-fill fields on document creation. This is useful when you want to create a document from an existing template and pre-fill the fields with specific values. + +To pre-fill a field, you need to make a `POST` request to the `/api/v1/templates/{templateId}/generate-document` endpoint with the field information. Here's an example: + +```json +{ + "title": "my-document.pdf", + "recipients": [ + { + "id": 3, + "name": "Example User", + "email": "example@documenso.com", + "signingOrder": 1, + "role": "SIGNER" + } + ], + "prefillFields": [ + { + "id": 21, + "type": "text", + "label": "my-label", + "placeholder": "my-placeholder", + "value": "my-value" + }, + { + "id": 22, + "type": "number", + "label": "my-label", + "placeholder": "my-placeholder", + "value": "123" + }, + { + "id": 23, + "type": "checkbox", + "label": "my-label", + "placeholder": "my-placeholder", + "value": ["option-1", "option-2"] + } + ] +} +``` + +Check out the endpoint in the [API V1 documentation](https://app.documenso.com/api/v1/openapi#:~:text=/%7BtemplateId%7D/-,generate,-%2Ddocument). + +### API V2 + +For API V2, you need to make a `POST` request to the `/api/v2-beta/template/use` endpoint with the field(s) information. Here's an example: + +```json +{ + "templateId": 111, + "recipients": [ + { + "id": 3, + "name": "Example User", + "email": "example@documenso.com", + "signingOrder": 1, + "role": "SIGNER" + } + ], + "prefillFields": [ + { + "id": 21, + "type": "text", + "label": "my-label", + "placeholder": "my-placeholder", + "value": "my-value" + }, + { + "id": 22, + "type": "number", + "label": "my-label", + "placeholder": "my-placeholder", + "value": "123" + }, + { + "id": 23, + "type": "checkbox", + "label": "my-label", + "placeholder": "my-placeholder", + "value": ["option-1", "option-2"] + } + ] +} +``` + +Check out the endpoint in the [API V2 documentation](https://openapi.documenso.com/reference#tag/template/POST/template/use). diff --git a/apps/documentation/providers/plausible.tsx b/apps/documentation/providers/plausible.tsx index dceaa4d93..ca8f3c691 100644 --- a/apps/documentation/providers/plausible.tsx +++ b/apps/documentation/providers/plausible.tsx @@ -1,5 +1,3 @@ -'use client'; - import React from 'react'; import NextPlausibleProvider from 'next-plausible'; diff --git a/apps/openpage-api/lib/add-zero-month.ts b/apps/openpage-api/lib/add-zero-month.ts new file mode 100644 index 000000000..eb5914599 --- /dev/null +++ b/apps/openpage-api/lib/add-zero-month.ts @@ -0,0 +1,54 @@ +import { DateTime } from 'luxon'; + +export interface TransformedData { + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + }>; +} + +export function addZeroMonth(transformedData: TransformedData): TransformedData { + const result = { + labels: [...transformedData.labels], + datasets: transformedData.datasets.map((dataset) => ({ + label: dataset.label, + data: [...dataset.data], + })), + }; + + if (result.labels.length === 0) { + return result; + } + + if (result.datasets.every((dataset) => dataset.data[0] === 0)) { + return result; + } + + try { + let firstMonth = DateTime.fromFormat(result.labels[0], 'MMM yyyy'); + if (!firstMonth.isValid) { + const formats = ['MMM yyyy', 'MMMM yyyy', 'MM/yyyy', 'yyyy-MM']; + + for (const format of formats) { + firstMonth = DateTime.fromFormat(result.labels[0], format); + if (firstMonth.isValid) break; + } + + if (!firstMonth.isValid) { + console.warn(`Could not parse date: "${result.labels[0]}"`); + return transformedData; + } + } + + const zeroMonth = firstMonth.minus({ months: 1 }).toFormat('MMM yyyy'); + result.labels.unshift(zeroMonth); + result.datasets.forEach((dataset) => { + dataset.data.unshift(0); + }); + + return result; + } catch (error) { + return transformedData; + } +} diff --git a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts index 885842101..f429b0a54 100644 --- a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts +++ b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts @@ -3,6 +3,8 @@ import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; +import { addZeroMonth } from '../add-zero-month'; + export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely .selectFrom('Document') @@ -35,7 +37,7 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' ], }; - return transformedData; + return addZeroMonth(transformedData); }; export type GetCompletedDocumentsMonthlyResult = Awaited< diff --git a/apps/openpage-api/lib/growth/get-signer-conversion.ts b/apps/openpage-api/lib/growth/get-signer-conversion.ts index aca2decb8..c70600179 100644 --- a/apps/openpage-api/lib/growth/get-signer-conversion.ts +++ b/apps/openpage-api/lib/growth/get-signer-conversion.ts @@ -2,6 +2,8 @@ import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; +import { addZeroMonth } from '../add-zero-month'; + export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely .selectFrom('Recipient') @@ -34,7 +36,7 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = ], }; - return transformedData; + return addZeroMonth(transformedData); }; export type GetSignerConversionMonthlyResult = Awaited< diff --git a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts index 6d4e526cb..9eba7311f 100644 --- a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts +++ b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts @@ -2,6 +2,8 @@ import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; +import { addZeroMonth } from '../add-zero-month'; + export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely .selectFrom('User') @@ -32,7 +34,7 @@ export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count ], }; - return transformedData; + return addZeroMonth(transformedData); }; export type GetUserMonthlyGrowthResult = Awaited>; diff --git a/apps/openpage-api/lib/transform-data.ts b/apps/openpage-api/lib/transform-data.ts index b5f0cd838..079ed4f6e 100644 --- a/apps/openpage-api/lib/transform-data.ts +++ b/apps/openpage-api/lib/transform-data.ts @@ -1,5 +1,7 @@ import { DateTime } from 'luxon'; +import { addZeroMonth } from './add-zero-month'; + type MetricKeys = { stars: number; forks: number; @@ -37,31 +39,77 @@ export function transformData({ data: DataEntry; metric: MetricKey; }): TransformData { - const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => { - const [yearA, monthA] = dateA.split('-').map(Number); - const [yearB, monthB] = dateB.split('-').map(Number); + try { + if (!data || Object.keys(data).length === 0) { + return { + labels: [], + datasets: [{ label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, data: [] }], + }; + } - return DateTime.local(yearA, monthA).toMillis() - DateTime.local(yearB, monthB).toMillis(); - }); + const sortedEntries = Object.entries(data).sort(([dateA], [dateB]) => { + try { + const [yearA, monthA] = dateA.split('-').map(Number); + const [yearB, monthB] = dateB.split('-').map(Number); - const labels = sortedEntries.map(([date]) => { - const [year, month] = date.split('-'); - const dateTime = DateTime.fromObject({ - year: Number(year), - month: Number(month), + if (isNaN(yearA) || isNaN(monthA) || isNaN(yearB) || isNaN(monthB)) { + console.warn(`Invalid date format: ${dateA} or ${dateB}`); + return 0; + } + + return DateTime.local(yearA, monthA).toMillis() - DateTime.local(yearB, monthB).toMillis(); + } catch (error) { + console.error('Error sorting entries:', error); + return 0; + } }); - return dateTime.toFormat('MMM yyyy'); - }); - return { - labels, - datasets: [ - { - label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, - data: sortedEntries.map(([_, stats]) => stats[metric]), - }, - ], - }; + const labels = sortedEntries.map(([date]) => { + try { + const [year, month] = date.split('-'); + + if (!year || !month || isNaN(Number(year)) || isNaN(Number(month))) { + console.warn(`Invalid date format: ${date}`); + return date; + } + + const dateTime = DateTime.fromObject({ + year: Number(year), + month: Number(month), + }); + + if (!dateTime.isValid) { + console.warn(`Invalid DateTime object for: ${date}`); + return date; + } + + return dateTime.toFormat('MMM yyyy'); + } catch (error) { + console.error('Error formatting date:', error, date); + return date; + } + }); + + const transformedData = { + labels, + datasets: [ + { + label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, + data: sortedEntries.map(([_, stats]) => { + const value = stats[metric]; + return typeof value === 'number' && !isNaN(value) ? value : 0; + }), + }, + ], + }; + + return addZeroMonth(transformedData); + } catch (error) { + return { + labels: [], + datasets: [{ label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, data: [] }], + }; + } } // To be on the safer side diff --git a/apps/remix/.bin/build.sh b/apps/remix/.bin/build.sh index d7e4c6134..ada8e5525 100755 --- a/apps/remix/.bin/build.sh +++ b/apps/remix/.bin/build.sh @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash # Exit on error. -set -eo pipefail +set -e SCRIPT_DIR="$(readlink -f "$(dirname "$0")")" WEB_APP_DIR="$SCRIPT_DIR/.." diff --git a/apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx b/apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx index 0c6356f9a..1240a0dc0 100644 --- a/apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx +++ b/apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx @@ -1,4 +1,7 @@ +import { zodResolver } from '@hookform/resolvers/zod'; import { Trans } from '@lingui/react/macro'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -9,64 +12,171 @@ import { DialogHeader, DialogTitle, } 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 { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure'; +export type NextSigner = { + name: string; + email: string; +}; + type ConfirmationDialogProps = { isOpen: boolean; onClose: () => void; - onConfirm: () => void; + onConfirm: (nextSigner?: NextSigner) => void; hasUninsertedFields: boolean; isSubmitting: boolean; + allowDictateNextSigner?: boolean; + defaultNextSigner?: NextSigner; }; +const ZNextSignerFormSchema = z.object({ + name: z.string().min(1, 'Name is required'), + email: z.string().email('Invalid email address'), +}); + +type TNextSignerFormSchema = z.infer; + export function AssistantConfirmationDialog({ isOpen, onClose, onConfirm, hasUninsertedFields, isSubmitting, + allowDictateNextSigner = false, + defaultNextSigner, }: ConfirmationDialogProps) { + const form = useForm({ + resolver: zodResolver(ZNextSignerFormSchema), + defaultValues: { + name: defaultNextSigner?.name ?? '', + email: defaultNextSigner?.email ?? '', + }, + }); + const onOpenChange = () => { if (isSubmitting) { return; } + form.reset({ + name: defaultNextSigner?.name ?? '', + email: defaultNextSigner?.email ?? '', + }); + onClose(); }; + const handleSubmit = () => { + // Validate the form and submit it if dictate signer is enabled. + if (allowDictateNextSigner) { + void form.handleSubmit(onConfirm)(); + return; + } + + onConfirm(); + }; + return ( - - - Complete Document - - - - Are you sure you want to complete the document? This action cannot be undone. Please - ensure that you have completed prefilling all relevant fields before proceeding. - - - +
+ +
+ + + Complete Document + + + + Are you sure you want to complete the document? This action cannot be undone. + Please ensure that you have completed prefilling all relevant fields before + proceeding. + + + -
- -
+
+ {allowDictateNextSigner && ( +
+

+ The next recipient to sign this document will be{' '} +

- - - - +
+ ( + + + Name + + + + + + + )} + /> + + ( + + + Email + + + + + + + )} + /> +
+
+ )} + + +
+ + + + + +
+
+
); diff --git a/apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx b/apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx new file mode 100644 index 000000000..7beecdcbe --- /dev/null +++ b/apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx @@ -0,0 +1,355 @@ +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { DocumentDistributionMethod } from '@prisma/client'; +import { InfoIcon } from 'lucide-react'; +import type { Control } from 'react-hook-form'; +import { useFormContext } from 'react-hook-form'; + +import { DATE_FORMATS } from '@documenso/lib/constants/date-formats'; +import { DOCUMENT_SIGNATURE_TYPES } from '@documenso/lib/constants/document'; +import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n'; +import { TIME_ZONES } from '@documenso/lib/constants/time-zones'; +import { DocumentEmailCheckboxes } from '@documenso/ui/components/document/document-email-checkboxes'; +import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper'; +import { Combobox } from '@documenso/ui/primitives/combobox'; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; +import { Textarea } from '@documenso/ui/primitives/textarea'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; + +import { useConfigureDocument } from './configure-document-context'; +import type { TConfigureEmbedFormSchema } from './configure-document-view.types'; + +interface ConfigureDocumentAdvancedSettingsProps { + control: Control; + isSubmitting: boolean; +} + +export const ConfigureDocumentAdvancedSettings = ({ + control, + isSubmitting, +}: ConfigureDocumentAdvancedSettingsProps) => { + const { _ } = useLingui(); + + const form = useFormContext(); + const { features } = useConfigureDocument(); + + const { watch, setValue } = form; + + // Lift watch() calls to reduce re-renders + const distributionMethod = watch('meta.distributionMethod'); + const emailSettings = watch('meta.emailSettings'); + const isEmailDistribution = distributionMethod === DocumentDistributionMethod.EMAIL; + + return ( +
+

+ Advanced Settings +

+ +

+ Configure additional options and preferences +

+ + + + + General + + + {features.allowConfigureCommunication && ( + + Communication + + )} + + + +
+ {/* ( + + + External ID + + + + + + + Add an external ID to the document. This can be used to identify the + document in external systems. + + + + + + + + + + )} + /> */} + + {features.allowConfigureSignatureTypes && ( + ( + + + Allowed Signature Types + + + ({ + label: _(option.label), + value: option.value, + }))} + selectedValues={field.value} + onChange={field.onChange} + className="bg-background w-full" + emptySelectionPlaceholder="Select signature types" + /> + + + + )} + /> + )} + + {features.allowConfigureLanguage && ( + ( + + + Language + + + + + + + )} + /> + )} + + {features.allowConfigureDateFormat && ( + ( + + + Date Format + + + + + + + )} + /> + )} + + {features.allowConfigureTimezone && ( + ( + + + Time Zone + + + value && field.onChange(value)} + disabled={isSubmitting} + /> + + + + )} + /> + )} + + {features.allowConfigureRedirectUrl && ( + ( + + + Redirect URL + + + + + + + Add a URL to redirect the user to once the document is signed + + + + + + + + + + )} + /> + )} +
+
+ + {features.allowConfigureCommunication && ( + +
+ ( + + + Distribution Method + + + + + + + + + Choose how to distribute your document to recipients. Email will send + notifications, None will generate signing links for manual distribution. + + + + + + )} + /> + +
+ ( + + + + Subject (Optional) + + + + + + + + )} + /> + + ( + + + + Message (Optional) + + + +