mirror of
https://github.com/documenso/documenso.git
synced 2026-07-13 06:25:01 +10:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43486d8448 | |||
| 4d3d1b8d14 | |||
| 0387f3c20a | |||
| c5032d0c43 | |||
| 3bd34964cd | |||
| fe93b11a2c | |||
| 7638faf27b | |||
| 8fca029d96 | |||
| bac2bf11f4 | |||
| d93b2a70a7 | |||
| 5da915da38 | |||
| dcaecf1fc5 | |||
| f70b76d8b8 | |||
| 93137c6396 | |||
| d058b7c705 | |||
| b51f562224 | |||
| f80aa4bf72 |
@@ -1,14 +1,19 @@
|
|||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main', 'feat/rr7']
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ['main']
|
branches: ['main']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e_tests:
|
e2e_tests:
|
||||||
name: 'E2E Tests'
|
name: 'E2E Tests'
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: warp-ubuntu-2204-x64-16x
|
runs-on: warp-ubuntu-2204-x64-8x
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -28,9 +33,6 @@ jobs:
|
|||||||
- name: Seed the database
|
- name: Seed the database
|
||||||
run: npm run prisma:seed
|
run: npm run prisma:seed
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Install playwright browsers
|
- name: Install playwright browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: test-results
|
name: test-results
|
||||||
path: 'packages/app-tests/**/test-results/*'
|
path: 'packages/app-tests/**/test-results/*'
|
||||||
retention-days: 30
|
retention-days: 7
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ jobs:
|
|||||||
environment: Translations
|
environment: Translations
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -26,12 +27,54 @@ jobs:
|
|||||||
- name: Extract translations
|
- name: Extract translations
|
||||||
run: npm run translate:extract
|
run: npm run translate:extract
|
||||||
|
|
||||||
- name: Check and commit any files created
|
- name: Commit changes and push to reserved branch
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BRANCH="chore/extract-translations"
|
||||||
|
|
||||||
git config --global user.name 'github-actions'
|
git config --global user.name 'github-actions'
|
||||||
git config --global user.email 'github-actions@documenso.com'
|
git config --global user.email 'github-actions@documenso.com'
|
||||||
|
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
# Create branch locally (always reset to main)
|
||||||
|
git checkout -B "$BRANCH" origin/main
|
||||||
|
|
||||||
|
# Stage translation output
|
||||||
git add packages/lib/translations
|
git add packages/lib/translations
|
||||||
git diff --staged --quiet --exit-code || (git commit -m "chore: extract translations" && git push)
|
|
||||||
|
# If no changes, exit early
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No translation changes found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit fresh snapshot
|
||||||
|
git commit -m "chore: extract translations"
|
||||||
|
|
||||||
|
# Force push reserved branch
|
||||||
|
git push origin "$BRANCH" --force
|
||||||
|
|
||||||
|
# Does a PR already exist?
|
||||||
|
EXISTING_PR=$(gh pr list \
|
||||||
|
--state open \
|
||||||
|
--head "$BRANCH" \
|
||||||
|
--json number \
|
||||||
|
--jq '.[0].number // empty')
|
||||||
|
|
||||||
|
if [ -z "$EXISTING_PR" ]; then
|
||||||
|
echo "No existing PR — creating new one."
|
||||||
|
gh pr create \
|
||||||
|
--title "chore: extract translations" \
|
||||||
|
--body "Automated translation extraction" \
|
||||||
|
--base main \
|
||||||
|
--head "$BRANCH"
|
||||||
|
else
|
||||||
|
echo "PR #$EXISTING_PR already exists — not creating a new one."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Compile translations
|
- name: Compile translations
|
||||||
id: compile_translations
|
id: compile_translations
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ The dropdown/select field collects a single choice from a list of options.
|
|||||||
|
|
||||||
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
Place the dropdown/select field on the document where you want the signer to select a choice. The dropdown/select field comes with additional settings that can be configured.
|
||||||
|
|
||||||
{/*  */}
|

|
||||||
|
|
||||||
The dropdown/select field settings include:
|
The dropdown/select field settings include:
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
|
||||||
|
|
||||||
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
type AiFeaturesEnableDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onEnabled: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiFeaturesEnableDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onEnabled,
|
||||||
|
}: AiFeaturesEnableDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const team = useCurrentTeam();
|
||||||
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
|
const isTeamAdmin = team.currentTeamRole === TeamMemberRole.ADMIN;
|
||||||
|
const isOrganisationAdmin = organisation.currentOrganisationRole === OrganisationMemberRole.ADMIN;
|
||||||
|
const canEnableAiFeatures = isTeamAdmin || isOrganisationAdmin;
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { mutateAsync: updateTeamSettings, isPending: isUpdatingTeamSettings } =
|
||||||
|
trpc.team.settings.update.useMutation();
|
||||||
|
const { mutateAsync: updateOrganisationSettings, isPending: isUpdatingOrganisationSettings } =
|
||||||
|
trpc.organisation.settings.update.useMutation();
|
||||||
|
|
||||||
|
const isSubmitting = isUpdatingTeamSettings || isUpdatingOrganisationSettings;
|
||||||
|
|
||||||
|
const onEnableClick = async () => {
|
||||||
|
if (!canEnableAiFeatures) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTeamAdmin) {
|
||||||
|
await updateTeamSettings({
|
||||||
|
teamId: team.id,
|
||||||
|
data: { aiFeaturesEnabled: true },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await updateOrganisationSettings({
|
||||||
|
organisationId: organisation.id,
|
||||||
|
data: { aiFeaturesEnabled: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnabled();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to enable AI features', err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: t`We couldn't enable AI features right now. Please try again.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Enable AI features</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
Turn on AI detection to automatically find recipients and fields in your documents. AI
|
||||||
|
providers do not retain your data for training.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Alert variant="neutral">
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
Your document content will be sent securely to our AI provider solely for detection
|
||||||
|
and will not be stored or used for training.
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{canEnableAiFeatures ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
You're an admin. You can enable AI features for this team right away. Everyone on
|
||||||
|
the team will see AI detection once enabled.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans>
|
||||||
|
AI features are disabled for your team. Please ask your team owner or organisation
|
||||||
|
owner to enable them.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{canEnableAiFeatures ? (
|
||||||
|
<Button type="button" onClick={() => void onEnableClick()} loading={isSubmitting}>
|
||||||
|
<Trans>Enable AI features</Trans>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
|
import { CheckIcon, FormInputIcon, ShieldCheckIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types';
|
||||||
@@ -232,10 +232,19 @@ export const AiFieldDetectionDialog = ({
|
|||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||||
<Trans>
|
<Plural
|
||||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
value={progress.fieldsDetected}
|
||||||
{progress.fieldsDetected} field(s) found
|
one={
|
||||||
</Trans>
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # field found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # fields found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -279,7 +288,11 @@ export const AiFieldDetectionDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<Trans>We found {detectedFields.length} field(s) in your document.</Trans>
|
<Plural
|
||||||
|
value={detectedFields.length}
|
||||||
|
one="We found # field in your document."
|
||||||
|
other="We found # fields in your document."
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-4 divide-y rounded-lg border">
|
<ul className="mt-4 divide-y rounded-lg border">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
|
import { CheckIcon, ShieldCheckIcon, UserIcon, XIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
@@ -190,10 +190,19 @@ export const AiRecipientDetectionDialog = ({
|
|||||||
|
|
||||||
{progress && (
|
{progress && (
|
||||||
<p className="mt-2 text-xs text-muted-foreground/60">
|
<p className="mt-2 text-xs text-muted-foreground/60">
|
||||||
<Trans>
|
<Plural
|
||||||
Page {progress.pagesProcessed} of {progress.totalPages} -{' '}
|
value={progress.recipientsDetected}
|
||||||
{progress.recipientsDetected} recipient(s) found
|
one={
|
||||||
</Trans>
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # recipient found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
other={
|
||||||
|
<Trans>
|
||||||
|
Page {progress.pagesProcessed} of {progress.totalPages} - # recipients found
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -237,9 +246,11 @@ export const AiRecipientDetectionDialog = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Plural
|
||||||
We found {detectedRecipients.length} recipient(s) in your document.
|
value={detectedRecipients.length}
|
||||||
</Trans>
|
one="We found # recipient in your document."
|
||||||
|
other="We found # recipients in your document."
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-4 divide-y rounded-lg border">
|
<ul className="mt-4 divide-y rounded-lg border">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import * as z from 'zod';
|
|||||||
|
|
||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -129,18 +130,43 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
const distributionMethod = watch('meta.distributionMethod');
|
const distributionMethod = watch('meta.distributionMethod');
|
||||||
|
|
||||||
|
const recipientsWithIndex = useMemo(
|
||||||
|
() =>
|
||||||
|
envelope.recipients.map((recipient, index) => ({
|
||||||
|
...recipient,
|
||||||
|
index,
|
||||||
|
})),
|
||||||
|
[envelope.recipients],
|
||||||
|
);
|
||||||
|
|
||||||
const recipientsMissingSignatureFields = useMemo(
|
const recipientsMissingSignatureFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
envelope.recipients.filter(
|
recipientsWithIndex.filter(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.role === RecipientRole.SIGNER &&
|
recipient.role === RecipientRole.SIGNER &&
|
||||||
!envelope.fields.some(
|
!envelope.fields.some(
|
||||||
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
[envelope.recipients, envelope.fields],
|
[recipientsWithIndex, envelope.fields],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of recipients who must have an email due to having auth enabled.
|
||||||
|
*/
|
||||||
|
const recipientsMissingRequiredEmail = useMemo(() => {
|
||||||
|
return recipientsWithIndex.filter((recipient) => {
|
||||||
|
const auth = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) && !recipient.email
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [recipientsWithIndex, envelope.authOptions]);
|
||||||
|
|
||||||
const invalidEnvelopeCode = useMemo(() => {
|
const invalidEnvelopeCode = useMemo(() => {
|
||||||
if (recipientsMissingSignatureFields.length > 0) {
|
if (recipientsMissingSignatureFields.length > 0) {
|
||||||
return 'MISSING_SIGNATURES';
|
return 'MISSING_SIGNATURES';
|
||||||
@@ -150,8 +176,12 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
return 'MISSING_RECIPIENTS';
|
return 'MISSING_RECIPIENTS';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipientsMissingRequiredEmail.length > 0) {
|
||||||
|
return 'MISSING_REQUIRED_EMAIL';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [envelope.recipients, envelope.fields, recipientsMissingSignatureFields]);
|
}, [envelope.recipients, recipientsMissingRequiredEmail, recipientsMissingSignatureFields]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
||||||
try {
|
try {
|
||||||
@@ -444,7 +474,22 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
<ul className="ml-2 mt-1 list-inside list-disc">
|
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||||
{recipientsMissingSignatureFields.map((recipient) => (
|
{recipientsMissingSignatureFields.map((recipient) => (
|
||||||
<li key={recipient.id}>{recipient.email}</li>
|
<li key={recipient.id}>
|
||||||
|
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
))
|
||||||
|
.with('MISSING_REQUIRED_EMAIL', () => (
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>The following recipients require an email address:</Trans>
|
||||||
|
|
||||||
|
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||||
|
{recipientsMissingRequiredEmail.map((recipient) => (
|
||||||
|
<li key={recipient.id}>
|
||||||
|
{recipient.email || recipient.name || t`Recipient ${recipient.index + 1}`}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import {
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
const ZSignFieldEmailFormSchema = z.object({
|
const ZSignFieldEmailFormSchema = z.object({
|
||||||
email: z.string().min(1, { message: msg`Email is required`.id }),
|
email: z
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.min(1, { message: msg`Email is required`.id }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type TSignFieldEmailFormSchema = z.infer<typeof ZSignFieldEmailFormSchema>;
|
type TSignFieldEmailFormSchema = z.infer<typeof ZSignFieldEmailFormSchema>;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -65,7 +66,7 @@ const ZAddRecipientsForNewDocumentSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
email: z.string().email(),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
@@ -100,12 +101,29 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
const { data: response, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getMany.useQuery(
|
||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
{
|
||||||
defaultValues: {
|
envelopeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
...SKIP_QUERY_BATCH_META,
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
enabled: open,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const envelopeItems = response?.data ?? [];
|
||||||
|
|
||||||
|
const generateDefaultFormValues = () => {
|
||||||
|
return {
|
||||||
distributeDocument: false,
|
distributeDocument: false,
|
||||||
useCustomDocument: false,
|
useCustomDocument: false,
|
||||||
customDocumentData: [],
|
customDocumentData: envelopeItems.map((item) => ({
|
||||||
|
title: item.title,
|
||||||
|
data: undefined,
|
||||||
|
envelopeItemId: item.id,
|
||||||
|
})),
|
||||||
recipients: recipients
|
recipients: recipients
|
||||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
@@ -124,7 +142,12 @@ export function TemplateUseDialog({
|
|||||||
signingOrder: recipient.signingOrder ?? undefined,
|
signingOrder: recipient.signingOrder ?? undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<TAddRecipientsForNewDocumentSchema>({
|
||||||
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
|
defaultValues: generateDefaultFormValues(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { replace, fields: localCustomDocumentData } = useFieldArray({
|
const { replace, fields: localCustomDocumentData } = useFieldArray({
|
||||||
@@ -132,19 +155,6 @@ export function TemplateUseDialog({
|
|||||||
name: 'customDocumentData',
|
name: 'customDocumentData',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: response, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getMany.useQuery(
|
|
||||||
{
|
|
||||||
envelopeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: (previousData) => previousData,
|
|
||||||
...SKIP_QUERY_BATCH_META,
|
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const envelopeItems = response?.data ?? [];
|
|
||||||
|
|
||||||
const { mutateAsync: createDocumentFromTemplate } =
|
const { mutateAsync: createDocumentFromTemplate } =
|
||||||
trpc.template.createDocumentFromTemplate.useMutation();
|
trpc.template.createDocumentFromTemplate.useMutation();
|
||||||
|
|
||||||
@@ -214,8 +224,8 @@ export function TemplateUseDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (open) {
|
||||||
form.reset();
|
form.reset(generateDefaultFormValues());
|
||||||
}
|
}
|
||||||
}, [open, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
@@ -322,7 +332,7 @@ export function TemplateUseDialog({
|
|||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
aria-label="Name"
|
aria-label="Name"
|
||||||
placeholder={recipients[index].name || _(msg`Name`)}
|
placeholder={recipients[index].name || _(msg`Recipient ${index + 1}`)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -349,7 +359,7 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||||
htmlFor="distributeDocument"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Send document</Trans>
|
<Trans>Send document</Trans>
|
||||||
@@ -358,7 +368,7 @@ export function TemplateUseDialog({
|
|||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
The document will be immediately sent to recipients if this
|
The document will be immediately sent to recipients if this
|
||||||
@@ -378,7 +388,7 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||||
htmlFor="distributeDocument"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Create as pending</Trans>
|
<Trans>Create as pending</Trans>
|
||||||
@@ -386,7 +396,7 @@ export function TemplateUseDialog({
|
|||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Create the document as pending and ready to sign.
|
Create the document as pending and ready to sign.
|
||||||
@@ -432,7 +442,7 @@ export function TemplateUseDialog({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||||
htmlFor="useCustomDocument"
|
htmlFor="useCustomDocument"
|
||||||
>
|
>
|
||||||
<Trans>Upload custom document</Trans>
|
<Trans>Upload custom document</Trans>
|
||||||
@@ -440,7 +450,7 @@ export function TemplateUseDialog({
|
|||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Upload a custom document to use instead of the template's default
|
Upload a custom document to use instead of the template's default
|
||||||
@@ -470,19 +480,19 @@ export function TemplateUseDialog({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="border-border bg-card hover:bg-accent/10 flex items-center gap-4 rounded-lg border p-4 transition-colors"
|
className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/10"
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg">
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||||
<FileTextIcon className="text-primary h-5 w-5" />
|
<FileTextIcon className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h4 className="text-foreground truncate text-sm font-medium">
|
<h4 className="truncate text-sm font-medium text-foreground">
|
||||||
{item.title}
|
{item.title}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
<div>
|
<div>
|
||||||
<Trans>
|
<Trans>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ZDocumentMetaDateFormatSchema,
|
ZDocumentMetaDateFormatSchema,
|
||||||
ZDocumentMetaLanguageSchema,
|
ZDocumentMetaLanguageSchema,
|
||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
||||||
|
|
||||||
// Define the schema for configuration
|
// Define the schema for configuration
|
||||||
@@ -55,7 +56,7 @@ export const ZConfigureTemplateEmbedFormSchema = ZConfigureEmbedFormSchema.exten
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
formId: z.string(),
|
formId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.union([z.string().length(0), z.string().email('Invalid email address')]),
|
email: ZRecipientEmailSchema,
|
||||||
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="text-muted-foreground text-sm"
|
className="text-sm text-muted-foreground"
|
||||||
title={
|
title={
|
||||||
signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined
|
signingToken ? _(msg`Click to copy signing link for sending to recipient`) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>{recipient.email}</p>
|
<p>{recipient.email || recipient.name}</p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-xs text-muted-foreground/70">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+20
-18
@@ -57,12 +57,13 @@ export type DocumentSigningCompleteDialogProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
directTemplatePayload?: {
|
recipientPayload?: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
buttonSize?: 'sm' | 'lg';
|
buttonSize?: 'sm' | 'lg';
|
||||||
position?: 'start' | 'end' | 'center';
|
position?: 'start' | 'end' | 'center';
|
||||||
|
disableNameInput?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ZNextSignerFormSchema = z.object({
|
const ZNextSignerFormSchema = z.object({
|
||||||
@@ -89,10 +90,11 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
recipient,
|
recipient,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
allowDictateNextSigner = false,
|
allowDictateNextSigner = false,
|
||||||
directTemplatePayload,
|
recipientPayload,
|
||||||
defaultNextSigner,
|
defaultNextSigner,
|
||||||
buttonSize = 'lg',
|
buttonSize = 'lg',
|
||||||
position,
|
position,
|
||||||
|
disableNameInput = false,
|
||||||
}: DocumentSigningCompleteDialogProps) => {
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@@ -113,11 +115,11 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const directRecipientForm = useForm<TDirectRecipientFormSchema>({
|
const recipientForm = useForm<TDirectRecipientFormSchema>({
|
||||||
resolver: zodResolver(ZDirectRecipientFormSchema),
|
resolver: zodResolver(ZDirectRecipientFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: directTemplatePayload?.name ?? '',
|
name: recipientPayload?.name ?? '',
|
||||||
email: directTemplatePayload?.email ?? '',
|
email: recipientPayload?.email ?? '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,16 +147,16 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
|
|
||||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||||
try {
|
try {
|
||||||
let directRecipient: { name: string; email: string } | undefined;
|
let recipientOverridePayload: { name: string; email: string } | undefined;
|
||||||
|
|
||||||
if (directTemplatePayload && !directTemplatePayload.email) {
|
if (recipientPayload && !recipientPayload.email) {
|
||||||
const isFormValid = await directRecipientForm.trigger();
|
const isFormValid = await recipientForm.trigger();
|
||||||
|
|
||||||
if (!isFormValid) {
|
if (!isFormValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
directRecipient = directRecipientForm.getValues();
|
recipientOverridePayload = recipientForm.getValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if 2FA is required
|
// Check if 2FA is required
|
||||||
@@ -168,7 +170,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
? { name: data.name, email: data.email }
|
? { name: data.name, email: data.email }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, directRecipient);
|
await onSignatureComplete(nextSigner, data.accessAuthOptions, recipientOverridePayload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = AppError.parseError(error);
|
const err = AppError.parseError(error);
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
<Trans>Are you sure?</Trans>
|
<Trans>Are you sure?</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<div className="text-muted-foreground max-w-[50ch]">
|
<div className="max-w-[50ch] text-muted-foreground">
|
||||||
{match(recipient.role)
|
{match(recipient.role)
|
||||||
.with(RecipientRole.VIEWER, () => (
|
.with(RecipientRole.VIEWER, () => (
|
||||||
<span className="inline-flex flex-wrap">
|
<span className="inline-flex flex-wrap">
|
||||||
@@ -250,19 +252,19 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="border-border bg-muted/50 rounded-lg border p-4 text-center">
|
<div className="rounded-lg border border-border bg-muted/50 p-4 text-center">
|
||||||
<p className="text-muted-foreground text-sm font-medium">{documentTitle}</p>
|
<p className="text-sm font-medium text-muted-foreground">{documentTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showTwoFactorForm && (
|
{!showTwoFactorForm && (
|
||||||
<>
|
<>
|
||||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||||
{directTemplatePayload && !directTemplatePayload.email && (
|
{recipientPayload && !recipientPayload.email && (
|
||||||
<Form {...directRecipientForm}>
|
<Form {...recipientForm}>
|
||||||
<div className="mb-4 flex flex-col gap-4">
|
<div className="mb-4 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4 md:flex-row">
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
<FormField
|
<FormField
|
||||||
control={directRecipientForm.control}
|
control={recipientForm.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
@@ -274,7 +276,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
{...field}
|
{...field}
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
placeholder={t`Enter your name`}
|
placeholder={t`Enter your name`}
|
||||||
disabled={isNameLocked}
|
disabled={isNameLocked || disableNameInput}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -284,7 +286,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={directRecipientForm.control}
|
control={recipientForm.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
import { FileTextIcon, SparklesIcon } from 'lucide-react';
|
||||||
import { Link, useSearchParams } from 'react-router';
|
import { Link, useRevalidator, useSearchParams } from 'react-router';
|
||||||
import { isDeepEqual } from 'remeda';
|
import { isDeepEqual } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/al
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Separator } from '@documenso/ui/primitives/separator';
|
import { Separator } from '@documenso/ui/primitives/separator';
|
||||||
|
|
||||||
|
import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog';
|
||||||
import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog';
|
import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog';
|
||||||
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form';
|
||||||
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form';
|
||||||
@@ -81,6 +82,8 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false);
|
||||||
|
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const selectedField = useMemo(
|
const selectedField = useMemo(
|
||||||
() => structuredClone(editorFields.selectedField),
|
() => structuredClone(editorFields.selectedField),
|
||||||
@@ -135,6 +138,22 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
|
editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDetectClick = () => {
|
||||||
|
if (!team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAiFieldDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiFeaturesEnabled = () => {
|
||||||
|
void revalidate().then(() => {
|
||||||
|
setIsAiEnableDialogOpen(false);
|
||||||
|
setIsAiFieldDialogOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full">
|
<div className="relative flex h-full">
|
||||||
<div className="flex w-full flex-col overflow-y-auto">
|
<div className="flex w-full flex-col overflow-y-auto">
|
||||||
@@ -230,34 +249,36 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{team.preferences.aiFeaturesEnabled && (
|
<Button
|
||||||
<>
|
type="button"
|
||||||
<Button
|
variant="outline"
|
||||||
type="button"
|
size="sm"
|
||||||
variant="outline"
|
className="mt-4 w-full"
|
||||||
size="sm"
|
onClick={onDetectClick}
|
||||||
className="mt-4 w-full"
|
disabled={envelope.status !== DocumentStatus.DRAFT}
|
||||||
onClick={() => setIsAiFieldDialogOpen(true)}
|
title={
|
||||||
disabled={envelope.status !== DocumentStatus.DRAFT}
|
envelope.status !== DocumentStatus.DRAFT
|
||||||
title={
|
? _(msg`You can only detect fields in draft envelopes`)
|
||||||
envelope.status !== DocumentStatus.DRAFT
|
: undefined
|
||||||
? _(msg`You can only detect fields in draft envelopes`)
|
}
|
||||||
: undefined
|
>
|
||||||
}
|
<SparklesIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||||
>
|
<Trans>Detect with AI</Trans>
|
||||||
<SparklesIcon className="-ml-1 mr-2 h-4 w-4" />
|
</Button>
|
||||||
<Trans>Detect with AI</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<AiFieldDetectionDialog
|
<AiFieldDetectionDialog
|
||||||
open={isAiFieldDialogOpen}
|
open={isAiFieldDialogOpen}
|
||||||
onOpenChange={setIsAiFieldDialogOpen}
|
onOpenChange={setIsAiFieldDialogOpen}
|
||||||
onComplete={onFieldDetectionComplete}
|
onComplete={onFieldDetectionComplete}
|
||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
teamId={envelope.teamId}
|
teamId={envelope.teamId}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
<AiFeaturesEnableDialog
|
||||||
|
open={isAiEnableDialogOpen}
|
||||||
|
onOpenChange={setIsAiEnableDialogOpen}
|
||||||
|
onEnabled={onAiFeaturesEnabled}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Field details section. */}
|
{/* Field details section. */}
|
||||||
|
|||||||
+71
-49
@@ -8,13 +8,13 @@ import {
|
|||||||
type SensorAPI,
|
type SensorAPI,
|
||||||
} from '@hello-pangea/dnd';
|
} from '@hello-pangea/dnd';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { plural } from '@lingui/core/macro';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, SparklesIcon, TrashIcon } from 'lucide-react';
|
||||||
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useRevalidator, useSearchParams } from 'react-router';
|
||||||
import { isDeepEqual, prop, sortBy } from 'remeda';
|
import { isDeepEqual, prop, sortBy } from 'remeda';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
ZRecipientActionAuthTypesSchema,
|
ZRecipientActionAuthTypesSchema,
|
||||||
ZRecipientAuthOptionsSchema,
|
ZRecipientAuthOptionsSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -62,6 +63,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog';
|
||||||
import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog';
|
import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-detection-dialog';
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
@@ -70,10 +72,7 @@ const ZEnvelopeRecipientsForm = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
formId: z.string().min(1),
|
formId: z.string().min(1),
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z
|
email: ZRecipientEmailSchema,
|
||||||
.string()
|
|
||||||
.email({ message: msg`Invalid email`.id })
|
|
||||||
.min(1),
|
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
@@ -99,11 +98,19 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
const [recipientSearchQuery, setRecipientSearchQuery] = useState('');
|
||||||
|
const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false);
|
||||||
|
|
||||||
// AI recipient detection dialog state
|
// AI recipient detection dialog state
|
||||||
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
const [isAiDialogOpen, setIsAiDialogOpen] = useState(() => searchParams.get('ai') === 'true');
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const onAiDialogOpenChange = (open: boolean) => {
|
const onAiDialogOpenChange = (open: boolean) => {
|
||||||
|
if (open && !team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
setIsAiDialogOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsAiDialogOpen(open);
|
setIsAiDialogOpen(open);
|
||||||
|
|
||||||
if (!open && searchParams.get('ai') === 'true') {
|
if (!open && searchParams.get('ai') === 'true') {
|
||||||
@@ -120,6 +127,22 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDetectRecipientsClick = () => {
|
||||||
|
if (!team.preferences.aiFeaturesEnabled) {
|
||||||
|
setIsAiEnableDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAiDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiFeaturesEnabled = () => {
|
||||||
|
void revalidate().then(() => {
|
||||||
|
setIsAiEnableDialogOpen(false);
|
||||||
|
setIsAiDialogOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500);
|
const debouncedRecipientSearchQuery = useDebouncedValue(recipientSearchQuery, 500);
|
||||||
|
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
@@ -228,12 +251,13 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
keyName: 'nativeId',
|
keyName: 'nativeId',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptySigners = useCallback(
|
const emptySignerIndex = watchedSigners.findIndex(
|
||||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
(signer) =>
|
||||||
[form],
|
!signer.name &&
|
||||||
|
!signer.email &&
|
||||||
|
envelope.fields.filter((field) => field.recipientId === signer.id).length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
|
|
||||||
const isUserAlreadyARecipient = watchedSigners.some(
|
const isUserAlreadyARecipient = watchedSigners.some(
|
||||||
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
||||||
);
|
);
|
||||||
@@ -331,8 +355,14 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Recipients added`,
|
title: plural(detectedRecipients.length, {
|
||||||
description: t`${detectedRecipients.length} recipient(s) have been added from AI detection.`,
|
one: `Recipient added`,
|
||||||
|
other: `Recipients added`,
|
||||||
|
}),
|
||||||
|
description: plural(detectedRecipients.length, {
|
||||||
|
one: `# recipient have been added from AI detection.`,
|
||||||
|
other: `# recipients have been added from AI detection.`,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -558,21 +588,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formValueSigners = formValues.signers || [];
|
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues);
|
||||||
|
|
||||||
// Remove the last signer if it's empty.
|
|
||||||
const nonEmptyRecipients = formValueSigners.filter((signer, i) => {
|
|
||||||
if (i === formValueSigners.length - 1 && signer.email === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({
|
|
||||||
...formValues,
|
|
||||||
signers: nonEmptyRecipients,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!validatedFormValues.success) {
|
if (!validatedFormValues.success) {
|
||||||
return;
|
return;
|
||||||
@@ -641,25 +657,27 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="flex flex-row items-center space-x-2">
|
||||||
{team.preferences.aiFeaturesEnabled && (
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
type="button"
|
||||||
type="button"
|
size="sm"
|
||||||
size="sm"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
onClick={onDetectRecipientsClick}
|
||||||
onClick={() => setIsAiDialogOpen(true)}
|
>
|
||||||
>
|
<SparklesIcon className="h-4 w-4" />
|
||||||
<SparklesIcon className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
{team.preferences.aiFeaturesEnabled ? (
|
||||||
<Trans>Detect recipients with AI</Trans>
|
<Trans>Detect recipients with AI</Trans>
|
||||||
</TooltipContent>
|
) : (
|
||||||
</Tooltip>
|
<Trans>Enable AI detection</Trans>
|
||||||
)}
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -736,9 +754,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isSubmitting || hasDocumentBeenSent}
|
||||||
isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -924,7 +940,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!showAdvancedSettings && index === 0 && (
|
{!showAdvancedSettings && index === 0 && (
|
||||||
<FormLabel required>
|
<FormLabel>
|
||||||
<Trans>Email</Trans>
|
<Trans>Email</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
@@ -978,7 +994,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<RecipientAutoCompleteInput
|
<RecipientAutoCompleteInput
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t`Name`}
|
placeholder={t`Recipient ${index + 1}`}
|
||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={
|
||||||
snapshot.isDragging ||
|
snapshot.isDragging ||
|
||||||
@@ -1118,6 +1134,12 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
teamId={envelope.teamId}
|
teamId={envelope.teamId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AiFeaturesEnableDialog
|
||||||
|
open={isAiEnableDialogOpen}
|
||||||
|
onOpenChange={setIsAiEnableDialogOpen}
|
||||||
|
onEnabled={onAiFeaturesEnabled}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import type { I18n } from '@lingui/core';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
@@ -39,8 +41,15 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
fields,
|
fields,
|
||||||
align = 'start',
|
align = 'start',
|
||||||
}: EnvelopeRecipientSelectorProps) => {
|
}: EnvelopeRecipientSelectorProps) => {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||||
|
|
||||||
|
const getRecipientLabel = useCallback(
|
||||||
|
(recipient: Recipient) => extractRecipientLabel(recipient, recipients, i18n),
|
||||||
|
[recipients],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -49,7 +58,7 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||||
getRecipientColorStyles(
|
getRecipientColorStyles(
|
||||||
Math.max(
|
Math.max(
|
||||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||||
@@ -59,16 +68,12 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedRecipient?.email && (
|
{selectedRecipient && (
|
||||||
<span className="flex-1 truncate text-left">
|
<span className="flex-1 truncate text-left">
|
||||||
{selectedRecipient?.name} ({selectedRecipient?.email})
|
{getRecipientLabel(selectedRecipient)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedRecipient?.email && (
|
|
||||||
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -105,7 +110,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
fields,
|
fields,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: EnvelopeRecipientSelectorCommandProps) => {
|
}: EnvelopeRecipientSelectorCommandProps) => {
|
||||||
const { t } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
|
|
||||||
const recipientsByRole = useCallback(() => {
|
const recipientsByRole = useCallback(() => {
|
||||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||||
@@ -154,6 +159,11 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
[fields, recipients],
|
[fields, recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getRecipientLabel = useCallback(
|
||||||
|
(recipient: Recipient) => extractRecipientLabel(recipient, recipients, i18n),
|
||||||
|
[recipients],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command
|
<Command
|
||||||
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
||||||
@@ -162,21 +172,21 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
<CommandInput placeholder={placeholder} />
|
<CommandInput placeholder={placeholder} />
|
||||||
|
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
<span className="text-muted-foreground inline-block px-4">
|
<span className="inline-block px-4 text-muted-foreground">
|
||||||
<Trans>No recipient matching this description was found.</Trans>
|
<Trans>No recipient matching this description was found.</Trans>
|
||||||
</span>
|
</span>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
||||||
<CommandGroup key={roleIndex}>
|
<CommandGroup key={roleIndex}>
|
||||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
|
||||||
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{roleRecipients.length === 0 && (
|
{roleRecipients.length === 0 && (
|
||||||
<div
|
<div
|
||||||
key={`${role}-empty`}
|
key={`${role}-empty`}
|
||||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
|
||||||
>
|
>
|
||||||
<Trans>No recipients with this role</Trans>
|
<Trans>No recipients with this role</Trans>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,18 +215,12 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn('text-foreground/70 truncate', {
|
className={cn('truncate text-foreground/70', {
|
||||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||||
'opacity-50': isRecipientDisabled(recipient.id),
|
'opacity-50': isRecipientDisabled(recipient.id),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{recipient.name && (
|
{getRecipientLabel(recipient)}
|
||||||
<span title={`${recipient.name} (${recipient.email})`}>
|
|
||||||
{recipient.name} ({recipient.email})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center justify-center">
|
<div className="ml-auto flex items-center justify-center">
|
||||||
@@ -234,7 +238,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
<Info className="z-50 ml-2 h-4 w-4" />
|
<Info className="z-50 ml-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
This document has already been sent to this recipient. You can no longer
|
This document has already been sent to this recipient. You can no longer
|
||||||
edit this recipient.
|
edit this recipient.
|
||||||
@@ -250,3 +254,22 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
</Command>
|
</Command>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractRecipientLabel = (recipient: Recipient, recipients: Recipient[], i18n: I18n) => {
|
||||||
|
if (recipient.name && recipient.email) {
|
||||||
|
return `${recipient.name} (${recipient.email})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient.name) {
|
||||||
|
return recipient.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient.email) {
|
||||||
|
return recipient.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since objects are basically pointers we can use `indexOf` rather than `findIndex`
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
return i18n._(msg`Recipient ${index + 1}`);
|
||||||
|
};
|
||||||
|
|||||||
+16
-4
@@ -80,12 +80,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
const handleOnCompleteClick = async (
|
const handleOnCompleteClick = async (
|
||||||
nextSigner?: { name: string; email: string },
|
nextSigner?: { name: string; email: string },
|
||||||
accessAuthOptions?: TRecipientAccessAuth,
|
accessAuthOptions?: TRecipientAccessAuth,
|
||||||
|
recipientDetails?: { name: string; email: string },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await completeDocument({
|
await completeDocument({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
|
recipientOverride: recipientDetails,
|
||||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,21 +207,30 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const directTemplatePayload = useMemo(() => {
|
const recipientPayload = useMemo(() => {
|
||||||
if (!isDirectTemplate) {
|
if (!isDirectTemplate) {
|
||||||
return;
|
return {
|
||||||
|
name:
|
||||||
|
recipient.name ||
|
||||||
|
recipient.fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
|
'',
|
||||||
|
email:
|
||||||
|
recipient.email ||
|
||||||
|
recipient.fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||||
|
'',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: fullName,
|
name: fullName,
|
||||||
email: email,
|
email: email,
|
||||||
};
|
};
|
||||||
}, [email, fullName, isDirectTemplate]);
|
}, [email, fullName, isDirectTemplate, recipient.email, recipient.name, recipient.fields]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentSigningCompleteDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isPending}
|
isSubmitting={isPending}
|
||||||
directTemplatePayload={directTemplatePayload}
|
recipientPayload={recipientPayload}
|
||||||
onSignatureComplete={
|
onSignatureComplete={
|
||||||
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
||||||
}
|
}
|
||||||
@@ -230,6 +241,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
allowDictateNextSigner={Boolean(
|
allowDictateNextSigner={Boolean(
|
||||||
nextRecipient && envelope.documentMeta.allowDictateNextSigner,
|
nextRecipient && envelope.documentMeta.allowDictateNextSigner,
|
||||||
)}
|
)}
|
||||||
|
disableNameInput={!isDirectTemplate && recipient.name !== ''}
|
||||||
defaultNextSigner={
|
defaultNextSigner={
|
||||||
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
<p className="text-sm text-muted-foreground">{recipient.email || recipient.name}</p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-xs text-muted-foreground/70">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,8 +107,8 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
fallbackText={recipientAbbreviation(recipient)}
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
<p className="text-sm text-muted-foreground">{recipient.email || recipient.name}</p>
|
||||||
<p className="text-muted-foreground/70 text-xs">
|
<p className="text-xs text-muted-foreground/70">
|
||||||
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-2
@@ -46,12 +46,16 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
const { getTheme } = await themeSessionResolver(request);
|
const { getTheme } = await themeSessionResolver(request);
|
||||||
|
|
||||||
let lang: SupportedLanguageCodes = await langCookie.parse(request.headers.get('cookie') ?? '');
|
const cookieHeader = request.headers.get('cookie') ?? '';
|
||||||
|
|
||||||
|
let lang: SupportedLanguageCodes = await langCookie.parse(cookieHeader);
|
||||||
|
|
||||||
if (!APP_I18N_OPTIONS.supportedLangs.includes(lang)) {
|
if (!APP_I18N_OPTIONS.supportedLangs.includes(lang)) {
|
||||||
lang = extractLocaleData({ headers: request.headers }).lang;
|
lang = extractLocaleData({ headers: request.headers }).lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableAnimations = cookieHeader.includes('__disable_animations=true');
|
||||||
|
|
||||||
let organisations = null;
|
let organisations = null;
|
||||||
|
|
||||||
if (session.isAuthenticated) {
|
if (session.isAuthenticated) {
|
||||||
@@ -62,6 +66,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
{
|
{
|
||||||
lang,
|
lang,
|
||||||
theme: getTheme(),
|
theme: getTheme(),
|
||||||
|
disableAnimations,
|
||||||
session: session.isAuthenticated
|
session: session.isAuthenticated
|
||||||
? {
|
? {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
@@ -92,7 +97,8 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutContent({ children }: { children: React.ReactNode }) {
|
export function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
const { publicEnv, session, lang, ...data } = useLoaderData<typeof loader>() || {};
|
const { publicEnv, session, lang, disableAnimations, ...data } =
|
||||||
|
useLoaderData<typeof loader>() || {};
|
||||||
|
|
||||||
const [theme] = useTheme();
|
const [theme] = useTheme();
|
||||||
|
|
||||||
@@ -111,6 +117,14 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {
|
|||||||
<meta name="google" content="notranslate" />
|
<meta name="google" content="notranslate" />
|
||||||
<PreventFlashOnWrongTheme ssrTheme={Boolean(data.theme)} />
|
<PreventFlashOnWrongTheme ssrTheme={Boolean(data.theme)} />
|
||||||
|
|
||||||
|
{disableAnimations && (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `*, *::before, *::after { animation: none !important; transition: none !important; }`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Fix: https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer */}
|
{/* Fix: https://stackoverflow.com/questions/21147149/flash-of-unstyled-content-fouc-in-firefox-only-is-ff-slow-renderer */}
|
||||||
<script>0</script>
|
<script>0</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { EnvelopeType, SigningStatus } from '@prisma/client';
|
import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-4 text-sm">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
<div>
|
<div>
|
||||||
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
<Trans>Created on</Trans>: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,8 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
|
|||||||
disabled={envelope.recipients.some(
|
disabled={envelope.recipients.some(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.signingStatus !== SigningStatus.SIGNED &&
|
recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||||
|
recipient.role !== RecipientRole.CC,
|
||||||
)}
|
)}
|
||||||
onClick={() => resealDocument({ id: envelope.id })}
|
onClick={() => resealDocument({ id: envelope.id })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export const loader = () => {
|
|||||||
export default function TeamsSettingsPage() {
|
export default function TeamsSettingsPage() {
|
||||||
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
const { isAiFeaturesConfigured } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
console.log('isAiFeaturesConfigured', isAiFeaturesConfigured);
|
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import satori from 'satori';
|
import satori from 'satori';
|
||||||
import sharp from 'sharp';
|
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
||||||
|
import { svgToPng } from '@documenso/lib/utils/images/svg-to-png';
|
||||||
|
|
||||||
import type { Route } from './+types/share.$slug.opengraph';
|
import type { Route } from './+types/share.$slug.opengraph';
|
||||||
|
|
||||||
@@ -181,8 +181,7 @@ export const loader = async ({ params }: Route.LoaderArgs) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert SVG to PNG using sharp
|
const pngBuffer = await svgToPng(svg.toString());
|
||||||
const pngBuffer = await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
|
||||||
|
|
||||||
return new Response(pngBuffer, {
|
return new Response(pngBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||||
|
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import type { Route } from './+types/branding.logo.organisation.$orgId';
|
import type { Route } from './+types/branding.logo.organisation.$orgId';
|
||||||
@@ -63,16 +62,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = await sharp(file)
|
const { content, contentType } = await loadLogo(file);
|
||||||
.toFormat('png', {
|
|
||||||
quality: 80,
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
return new Response(Buffer.from(img), {
|
return new Response(content, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png',
|
'Content-Type': contentType,
|
||||||
'Content-Length': img.length.toString(),
|
'Content-Length': content.length.toString(),
|
||||||
// Stale while revalidate for 1 hours to 24 hours
|
// Stale while revalidate for 1 hours to 24 hours
|
||||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||||
|
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||||
|
|
||||||
import type { Route } from './+types/branding.logo.team.$teamId';
|
import type { Route } from './+types/branding.logo.team.$teamId';
|
||||||
|
|
||||||
@@ -56,16 +55,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = await sharp(file)
|
const { content, contentType } = await loadLogo(file);
|
||||||
.toFormat('png', {
|
|
||||||
quality: 80,
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
return new Response(img, {
|
return new Response(content, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/png',
|
'Content-Type': contentType,
|
||||||
'Content-Length': img.length.toString(),
|
'Content-Length': content.length.toString(),
|
||||||
// Stale while revalidate for 1 hours to 24 hours
|
// Stale while revalidate for 1 hours to 24 hours
|
||||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|||||||
const token = url.searchParams.get('token') || '';
|
const token = url.searchParams.get('token') || '';
|
||||||
|
|
||||||
// We also know that the token is valid, but we need the userId + teamId
|
// We also know that the token is valid, but we need the userId + teamId
|
||||||
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
const result = await verifyEmbeddingPresignToken({ token, scope: `documentId:${id}` }).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Invalid token');
|
throw new Error('Invalid token');
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
|||||||
const token = url.searchParams.get('token') || '';
|
const token = url.searchParams.get('token') || '';
|
||||||
|
|
||||||
// We also know that the token is valid, but we need the userId + teamId
|
// We also know that the token is valid, but we need the userId + teamId
|
||||||
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
|
const result = await verifyEmbeddingPresignToken({ token, scope: `templateId:${id}` }).catch(
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Invalid token');
|
throw new Error('Invalid token');
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
"remix-themes": "^2.0.4",
|
"remix-themes": "^2.0.4",
|
||||||
"satori": "^0.18.3",
|
"satori": "^0.18.3",
|
||||||
"sharp": "0.34.5",
|
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^3.4.18",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"ua-parser-js": "^1.0.41",
|
"ua-parser-js": "^1.0.41",
|
||||||
@@ -108,5 +107,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "2.2.4"
|
"version": "2.2.6"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+277
-534
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.2.4",
|
"version": "2.2.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "2.2.4",
|
"version": "2.2.6",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "2.2.4",
|
"version": "2.2.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cantoo/pdf-lib": "^2.5.3",
|
"@cantoo/pdf-lib": "^2.5.3",
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -167,7 +167,6 @@
|
|||||||
"remeda": "^2.32.0",
|
"remeda": "^2.32.0",
|
||||||
"remix-themes": "^2.0.4",
|
"remix-themes": "^2.0.4",
|
||||||
"satori": "^0.18.3",
|
"satori": "^0.18.3",
|
||||||
"sharp": "0.34.5",
|
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^3.4.18",
|
||||||
"ts-pattern": "^5.9.0",
|
"ts-pattern": "^5.9.0",
|
||||||
"ua-parser-js": "^1.0.41",
|
"ua-parser-js": "^1.0.41",
|
||||||
@@ -666,58 +665,6 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aws-sdk/client-ses": {
|
|
||||||
"version": "3.936.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.936.0.tgz",
|
|
||||||
"integrity": "sha512-2toHYwRkcYGasPHYGwOwaIAa2Api/uFhmL3px0Tyt4bne2ilqhSwq+6a/0UVMd8JYwWaLMJolTbWKFt2jUlmGg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-crypto/sha256-browser": "5.2.0",
|
|
||||||
"@aws-crypto/sha256-js": "5.2.0",
|
|
||||||
"@aws-sdk/core": "3.936.0",
|
|
||||||
"@aws-sdk/credential-provider-node": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-host-header": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-logger": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-recursion-detection": "3.936.0",
|
|
||||||
"@aws-sdk/middleware-user-agent": "3.936.0",
|
|
||||||
"@aws-sdk/region-config-resolver": "3.936.0",
|
|
||||||
"@aws-sdk/types": "3.936.0",
|
|
||||||
"@aws-sdk/util-endpoints": "3.936.0",
|
|
||||||
"@aws-sdk/util-user-agent-browser": "3.936.0",
|
|
||||||
"@aws-sdk/util-user-agent-node": "3.936.0",
|
|
||||||
"@smithy/config-resolver": "^4.4.3",
|
|
||||||
"@smithy/core": "^3.18.5",
|
|
||||||
"@smithy/fetch-http-handler": "^5.3.6",
|
|
||||||
"@smithy/hash-node": "^4.2.5",
|
|
||||||
"@smithy/invalid-dependency": "^4.2.5",
|
|
||||||
"@smithy/middleware-content-length": "^4.2.5",
|
|
||||||
"@smithy/middleware-endpoint": "^4.3.12",
|
|
||||||
"@smithy/middleware-retry": "^4.4.12",
|
|
||||||
"@smithy/middleware-serde": "^4.2.6",
|
|
||||||
"@smithy/middleware-stack": "^4.2.5",
|
|
||||||
"@smithy/node-config-provider": "^4.3.5",
|
|
||||||
"@smithy/node-http-handler": "^4.4.5",
|
|
||||||
"@smithy/protocol-http": "^5.3.5",
|
|
||||||
"@smithy/smithy-client": "^4.9.8",
|
|
||||||
"@smithy/types": "^4.9.0",
|
|
||||||
"@smithy/url-parser": "^4.2.5",
|
|
||||||
"@smithy/util-base64": "^4.3.0",
|
|
||||||
"@smithy/util-body-length-browser": "^4.2.0",
|
|
||||||
"@smithy/util-body-length-node": "^4.2.1",
|
|
||||||
"@smithy/util-defaults-mode-browser": "^4.3.11",
|
|
||||||
"@smithy/util-defaults-mode-node": "^4.2.14",
|
|
||||||
"@smithy/util-endpoints": "^3.2.5",
|
|
||||||
"@smithy/util-middleware": "^4.2.5",
|
|
||||||
"@smithy/util-retry": "^4.2.5",
|
|
||||||
"@smithy/util-utf8": "^4.2.0",
|
|
||||||
"@smithy/util-waiter": "^4.2.5",
|
|
||||||
"tslib": "^2.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@aws-sdk/client-sesv2": {
|
"node_modules/@aws-sdk/client-sesv2": {
|
||||||
"version": "3.936.0",
|
"version": "3.936.0",
|
||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.936.0.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.936.0.tgz",
|
||||||
@@ -5963,12 +5910,6 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@one-ini/wasm": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||||
@@ -12161,6 +12102,7 @@
|
|||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -14407,21 +14349,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/render": {
|
"node_modules/@react-email/render": {
|
||||||
"version": "0.0.17",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.0.tgz",
|
||||||
"integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==",
|
"integrity": "sha512-rdjNj6iVzv8kRKDPFas+47nnoe6B40+nwukuXwY4FCwM7XBg6tmYr+chQryCuavUj2J65MMf6fztk1bxOUiSVA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"js-beautify": "^1.14.11",
|
"prettier": "^3.5.3"
|
||||||
"react-promise-suspense": "0.3.4"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/row": {
|
"node_modules/@react-email/row": {
|
||||||
@@ -17107,13 +17048,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/nodemailer": {
|
"node_modules/@types/nodemailer": {
|
||||||
"version": "6.4.21",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz",
|
||||||
"integrity": "sha512-Eix+sb/Nj28MNnWvO2X1OLrk5vuD4C9SMnb2Vf4itWnxphYeSceqkFX7IdmxTzn+dvmnNz7paMbg4Uc60wSfJg==",
|
"integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-ses": "^3.731.1",
|
"@aws-sdk/client-sesv2": "^3.839.0",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -17787,15 +17728,6 @@
|
|||||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -19861,6 +19793,7 @@
|
|||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -20063,22 +19996,6 @@
|
|||||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/config-chain": {
|
|
||||||
"version": "1.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
|
||||||
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ini": "^1.3.4",
|
|
||||||
"proto-list": "~1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/config-chain/node_modules/ini": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/consola": {
|
"node_modules/consola": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
@@ -21571,48 +21488,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/editorconfig": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@one-ini/wasm": "0.1.1",
|
|
||||||
"commander": "^10.0.0",
|
|
||||||
"minimatch": "9.0.1",
|
|
||||||
"semver": "^7.5.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"editorconfig": "bin/editorconfig"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/editorconfig/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/editorconfig/node_modules/minimatch": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -26314,117 +26189,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-beautify": {
|
|
||||||
"version": "1.15.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
|
|
||||||
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"config-chain": "^1.1.13",
|
|
||||||
"editorconfig": "^1.0.4",
|
|
||||||
"glob": "^10.4.2",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"nopt": "^7.2.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"css-beautify": "js/bin/css-beautify.js",
|
|
||||||
"html-beautify": "js/bin/html-beautify.js",
|
|
||||||
"js-beautify": "js/bin/js-beautify.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/glob": {
|
|
||||||
"version": "10.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
|
||||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"foreground-child": "^3.1.0",
|
|
||||||
"jackspeak": "^3.1.2",
|
|
||||||
"minimatch": "^9.0.4",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"package-json-from-dist": "^1.0.0",
|
|
||||||
"path-scurry": "^1.11.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"glob": "dist/esm/bin.mjs"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/jackspeak": {
|
|
||||||
"version": "3.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
|
||||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/cliui": "^8.0.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/lru-cache": {
|
|
||||||
"version": "10.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/minimatch": {
|
|
||||||
"version": "9.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-beautify/node_modules/path-scurry": {
|
|
||||||
"version": "1.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
|
||||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^10.2.0",
|
|
||||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-cookie": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
|
||||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-sdsl": {
|
"node_modules/js-sdsl": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
@@ -29480,21 +29244,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nopt": {
|
|
||||||
"version": "7.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
|
|
||||||
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nopt": "bin/nopt.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/normalize-package-data": {
|
"node_modules/normalize-package-data": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz",
|
||||||
@@ -31586,12 +31335,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proto-list": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||||
@@ -31927,263 +31670,6 @@
|
|||||||
"react": ">= 16.8 || 18.0.0"
|
"react": ">= 16.8 || 18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-email": {
|
|
||||||
"version": "5.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.4.tgz",
|
|
||||||
"integrity": "sha512-pI6b4ePMWC1AFtyd0gyVMsJ6Nhg4nnmDxVo1EQP+r011WQafU8UGAUfPUiE5Wh7x28+3yFw7qXCnD33y71guUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.27.0",
|
|
||||||
"@babel/traverse": "^7.27.0",
|
|
||||||
"chokidar": "^4.0.3",
|
|
||||||
"commander": "^13.0.0",
|
|
||||||
"conf": "^15.0.2",
|
|
||||||
"debounce": "^2.0.0",
|
|
||||||
"esbuild": "^0.25.0",
|
|
||||||
"glob": "^11.0.0",
|
|
||||||
"jiti": "2.4.2",
|
|
||||||
"log-symbols": "^7.0.0",
|
|
||||||
"mime-types": "^3.0.0",
|
|
||||||
"normalize-path": "^3.0.0",
|
|
||||||
"nypm": "0.6.0",
|
|
||||||
"ora": "^8.0.0",
|
|
||||||
"prompts": "2.4.2",
|
|
||||||
"socket.io": "^4.8.1",
|
|
||||||
"tsconfig-paths": "4.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"email": "dist/index.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ansi-regex": {
|
|
||||||
"version": "6.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
|
||||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/chokidar": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.16.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/commander": {
|
|
||||||
"version": "13.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
|
||||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/emoji-regex": {
|
|
||||||
"version": "10.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
|
||||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/is-interactive": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/is-unicode-supported": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/jiti": {
|
|
||||||
"version": "2.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
|
||||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/log-symbols": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-unicode-supported": "^2.0.0",
|
|
||||||
"yoctocolors": "^2.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/mime-db": {
|
|
||||||
"version": "1.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
|
||||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/mime-types": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "^1.54.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ora": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"cli-cursor": "^5.0.0",
|
|
||||||
"cli-spinners": "^2.9.2",
|
|
||||||
"is-interactive": "^2.0.0",
|
|
||||||
"is-unicode-supported": "^2.0.0",
|
|
||||||
"log-symbols": "^6.0.0",
|
|
||||||
"stdin-discarder": "^0.2.2",
|
|
||||||
"string-width": "^7.2.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ora/node_modules/log-symbols": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"is-unicode-supported": "^1.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/readdirp": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.18.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/string-width": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"emoji-regex": "^10.3.0",
|
|
||||||
"get-east-asian-width": "^1.0.0",
|
|
||||||
"strip-ansi": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/strip-ansi": {
|
|
||||||
"version": "7.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
|
||||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-regex": "^6.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-email/node_modules/tsconfig-paths": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"json5": "^2.2.2",
|
|
||||||
"minimist": "^1.2.6",
|
|
||||||
"strip-bom": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.66.1",
|
"version": "7.66.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.1.tgz",
|
||||||
@@ -37453,18 +36939,275 @@
|
|||||||
"@react-email/img": "0.0.11",
|
"@react-email/img": "0.0.11",
|
||||||
"@react-email/link": "0.0.12",
|
"@react-email/link": "0.0.12",
|
||||||
"@react-email/preview": "0.0.13",
|
"@react-email/preview": "0.0.13",
|
||||||
"@react-email/render": "0.0.17",
|
"@react-email/render": "2.0.0",
|
||||||
"@react-email/row": "0.0.12",
|
"@react-email/row": "0.0.12",
|
||||||
"@react-email/section": "0.0.16",
|
"@react-email/section": "0.0.16",
|
||||||
"@react-email/tailwind": "^2.0.1",
|
"@react-email/tailwind": "^2.0.1",
|
||||||
"@react-email/text": "0.1.5",
|
"@react-email/text": "0.1.5",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"react-email": "^5.0.4",
|
"react-email": "^5.0.6",
|
||||||
"resend": "^6.5.2"
|
"resend": "^6.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.21"
|
"@types/nodemailer": "^7.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/commander": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/is-interactive": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/is-unicode-supported": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/jiti": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/log-symbols": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-unicode-supported": "^2.0.0",
|
||||||
|
"yoctocolors": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ora": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"cli-cursor": "^5.0.0",
|
||||||
|
"cli-spinners": "^2.9.2",
|
||||||
|
"is-interactive": "^2.0.0",
|
||||||
|
"is-unicode-supported": "^2.0.0",
|
||||||
|
"log-symbols": "^6.0.0",
|
||||||
|
"stdin-discarder": "^0.2.2",
|
||||||
|
"string-width": "^7.2.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ora/node_modules/log-symbols": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"is-unicode-supported": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/react-email": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-DEGzWpEiC3CquPEaaEJuipNT3WZ9mK58rbkpOe4Slbgyf60PLa1wONnt5a3afbBBRbNdW2aYhIvVI41yS6UIRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.27.0",
|
||||||
|
"@babel/traverse": "^7.27.0",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"commander": "^13.0.0",
|
||||||
|
"conf": "^15.0.2",
|
||||||
|
"debounce": "^2.0.0",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"glob": "^11.0.0",
|
||||||
|
"jiti": "2.4.2",
|
||||||
|
"log-symbols": "^7.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"nypm": "0.6.0",
|
||||||
|
"ora": "^8.0.0",
|
||||||
|
"prompts": "2.4.2",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"tsconfig-paths": "4.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"email": "dist/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/email/node_modules/tsconfig-paths": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json5": "^2.2.2",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"strip-bom": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/eslint-config": {
|
"packages/eslint-config": {
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "2.2.4",
|
"version": "2.2.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import type { APIRequestContext } from 'playwright-core';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import type { CreateEmbeddingPresignTokenOptions } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
|
||||||
|
import type { VerifyEmbeddingPresignTokenOptions } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
@@ -17,18 +20,7 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await createPresignToken(request, token);
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
@@ -54,19 +46,9 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await createPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
expiresIn: 120, // 2 hours
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
expiresIn: 120, // 2 hours
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
@@ -92,19 +74,9 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await createPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
expiresIn: 0, // Immediate expiration
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
expiresIn: 0, // Immediate expiration
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.ok()).toBeTruthy();
|
expect(response.ok()).toBeTruthy();
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
@@ -129,18 +101,7 @@ test.describe('Embedding Presign API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// First create a token
|
// First create a token
|
||||||
const createResponse = await request.post(
|
const createResponse = await createPresignToken(request, token);
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
apiToken: token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(createResponse.ok()).toBeTruthy();
|
expect(createResponse.ok()).toBeTruthy();
|
||||||
const createResponseData = await createResponse.json();
|
const createResponseData = await createResponse.json();
|
||||||
@@ -150,18 +111,9 @@ test.describe('Embedding Presign API', () => {
|
|||||||
const presignToken = createResponseData.token;
|
const presignToken = createResponseData.token;
|
||||||
|
|
||||||
// Then verify it
|
// Then verify it
|
||||||
const verifyResponse = await request.post(
|
const verifyResponse = await verifyPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
|
token: presignToken,
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
token: presignToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(verifyResponse.ok()).toBeTruthy();
|
expect(verifyResponse.ok()).toBeTruthy();
|
||||||
expect(verifyResponse.status()).toBe(200);
|
expect(verifyResponse.status()).toBe(200);
|
||||||
@@ -183,18 +135,87 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expiresIn: null,
|
expiresIn: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await verifyPresignToken(request, token, {
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
|
token: 'invalid-token',
|
||||||
{
|
});
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
const responseData = await response.json();
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
console.log('Invalid token response:', responseData);
|
||||||
data: {
|
|
||||||
token: 'invalid-token',
|
expect(response.ok()).toBeTruthy();
|
||||||
},
|
expect(response.status()).toBe(200);
|
||||||
},
|
|
||||||
);
|
expect(responseData.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyEmbeddingPresignToken: should verify a valid scoped token', async ({ request }) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const { token } = await createApiToken({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
tokenName: 'test',
|
||||||
|
expiresIn: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// First create a token
|
||||||
|
const createResponse = await createPresignToken(request, token, {
|
||||||
|
scope: 'documentId:1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResponse.ok()).toBeTruthy();
|
||||||
|
const createResponseData = await createResponse.json();
|
||||||
|
|
||||||
|
console.log('Create response:', createResponseData);
|
||||||
|
|
||||||
|
const presignToken = createResponseData.token;
|
||||||
|
|
||||||
|
// Then verify it
|
||||||
|
const verifyResponse = await verifyPresignToken(request, token, {
|
||||||
|
token: presignToken,
|
||||||
|
scope: 'documentId:1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(verifyResponse.ok()).toBeTruthy();
|
||||||
|
expect(verifyResponse.status()).toBe(200);
|
||||||
|
|
||||||
|
const verifyResponseData = await verifyResponse.json();
|
||||||
|
|
||||||
|
console.log('Verify response:', verifyResponseData);
|
||||||
|
|
||||||
|
expect(verifyResponseData.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyEmbeddingPresignToken: should reject a scope mismatched token', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const { token } = await createApiToken({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
tokenName: 'test',
|
||||||
|
expiresIn: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// First create a token
|
||||||
|
const createResponse = await createPresignToken(request, token, {
|
||||||
|
scope: 'documentId:1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createResponse.ok()).toBeTruthy();
|
||||||
|
const createResponseData = await createResponse.json();
|
||||||
|
|
||||||
|
console.log('Create response:', createResponseData);
|
||||||
|
|
||||||
|
const presignToken = createResponseData.token;
|
||||||
|
|
||||||
|
// Then verify it
|
||||||
|
const response = await verifyPresignToken(request, token, {
|
||||||
|
token: presignToken,
|
||||||
|
scope: 'documentId:2',
|
||||||
|
});
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
@@ -206,3 +227,40 @@ test.describe('Embedding Presign API', () => {
|
|||||||
expect(responseData.success).toBe(false);
|
expect(responseData.success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createPresignToken = async (
|
||||||
|
request: APIRequestContext,
|
||||||
|
apiToken: string,
|
||||||
|
data?: Partial<CreateEmbeddingPresignTokenOptions>,
|
||||||
|
) => {
|
||||||
|
return await request.post(
|
||||||
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/create-presign-token`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
apiToken,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyPresignToken = async (
|
||||||
|
request: APIRequestContext,
|
||||||
|
apiToken: string,
|
||||||
|
data: VerifyEmbeddingPresignTokenOptions,
|
||||||
|
) => {
|
||||||
|
return await request.post(
|
||||||
|
`${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta/embedding/verify-presign-token`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { pick } from 'remeda';
|
|||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||||
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentDistributionMethod,
|
DocumentDistributionMethod,
|
||||||
@@ -23,7 +24,9 @@ import type {
|
|||||||
TCreateEnvelopePayload,
|
TCreateEnvelopePayload,
|
||||||
TCreateEnvelopeResponse,
|
TCreateEnvelopeResponse,
|
||||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||||
|
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
|
||||||
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
||||||
|
import type { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
|
||||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||||
|
|
||||||
@@ -144,6 +147,9 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
externalId: 'externalId',
|
externalId: 'externalId',
|
||||||
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
visibility: DocumentVisibility.MANAGER_AND_ABOVE,
|
||||||
globalAccessAuth: ['ACCOUNT'],
|
globalAccessAuth: ['ACCOUNT'],
|
||||||
|
// Ignore this error in the test since it doesn't actually exist in the PDF:
|
||||||
|
// - Error setting value for field hello: PDFDocument has no form field with the name "hello"
|
||||||
|
// We want to check if the form value is set in the DB.
|
||||||
formValues: {
|
formValues: {
|
||||||
hello: 'world',
|
hello: 'world',
|
||||||
},
|
},
|
||||||
@@ -262,8 +268,6 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(userB.email);
|
|
||||||
|
|
||||||
expect(envelope.envelopeItems.length).toBe(2);
|
expect(envelope.envelopeItems.length).toBe(2);
|
||||||
expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
|
expect(envelope.envelopeItems[0].title).toBe('field-meta.pdf');
|
||||||
expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');
|
expect(envelope.envelopeItems[1].title).toBe('field-font-alignment.pdf');
|
||||||
@@ -557,4 +561,543 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
userEmail: userA.email,
|
userEmail: userA.email,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Empty recipient tests', () => {
|
||||||
|
test('Create template envelope with empty email recipient', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Template with Empty Email Recipient',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Test Recipient',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
expect(createRecipientsRes.status()).toBe(200);
|
||||||
|
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
expect(recipient.email).toBe('');
|
||||||
|
expect(recipient.name).toBe('Test Recipient');
|
||||||
|
|
||||||
|
// Get envelope items to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create field for the recipient with empty email
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 100,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
expect(createFieldsRes.status()).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create document envelope with empty email recipient', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document with Empty Email Recipient',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Document Recipient No Email',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
expect(recipient.email).toBe('');
|
||||||
|
expect(recipient.name).toBe('Document Recipient No Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Update recipient to have empty email', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Update Recipient Email Test',
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
email: userA.email,
|
||||||
|
name: 'Test User',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Get the envelope to get recipient ID
|
||||||
|
const getRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getRes.json();
|
||||||
|
const recipientId = envelope.recipients[0].id;
|
||||||
|
|
||||||
|
// Update recipient to have empty email
|
||||||
|
const updateRequest: TUpdateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: recipientId,
|
||||||
|
email: '',
|
||||||
|
name: 'Updated Name No Email',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRes = await request.post(`${baseUrl}/envelope/recipient/update-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: updateRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateRes.ok()).toBeTruthy();
|
||||||
|
const updateResponse = await updateRes.json();
|
||||||
|
const updatedRecipient = updateResponse.data[0];
|
||||||
|
|
||||||
|
expect(updatedRecipient.email).toBe('');
|
||||||
|
expect(updatedRecipient.name).toBe('Updated Name No Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed recipients with and without emails', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Mixed Recipients Test',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create multiple recipients, some with email, some without
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: userA.email,
|
||||||
|
name: 'Recipient With Email',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Without Email 1',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: userB.email,
|
||||||
|
name: 'Another With Email',
|
||||||
|
role: RecipientRole.APPROVER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Without Email 2',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipients = recipientsResponse.data;
|
||||||
|
|
||||||
|
expect(recipients.length).toBe(4);
|
||||||
|
expect(recipients[0].email).toBe(userA.email.toLowerCase());
|
||||||
|
expect(recipients[1].email).toBe('');
|
||||||
|
expect(recipients[2].email).toBe(userB.email.toLowerCase());
|
||||||
|
expect(recipients[3].email).toBe('');
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create fields for all recipients including those without emails
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: recipients.map((recipient, index) => ({
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 0 + index,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Distribute envelope with empty email recipients', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document for Distribution with Empty Email',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipients with empty emails
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient One',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Two',
|
||||||
|
role: RecipientRole.APPROVER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipients = recipientsResponse.data;
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create fields for recipients
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: recipients.map((recipient, index) => ({
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 0 + index,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Distribute the envelope
|
||||||
|
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
} satisfies TDistributeEnvelopeRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(distributeRes.ok()).toBeTruthy();
|
||||||
|
expect(distributeRes.status()).toBe(200);
|
||||||
|
|
||||||
|
const distributeResponse = await distributeRes.json();
|
||||||
|
expect(distributeResponse.success).toBe(true);
|
||||||
|
expect(distributeResponse.id).toBe(createResponse.id);
|
||||||
|
expect(distributeResponse.recipients).toHaveLength(2);
|
||||||
|
|
||||||
|
// Verify recipients have empty emails and signing URLs
|
||||||
|
expect(distributeResponse.recipients[0].email).toBe('');
|
||||||
|
expect(distributeResponse.recipients[0].signingUrl).toBeTruthy();
|
||||||
|
expect(distributeResponse.recipients[1].email).toBe('');
|
||||||
|
expect(distributeResponse.recipients[1].signingUrl).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Distribute envelope with empty email recipient and auth requirements fails', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document with Auth Requirements',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email and TWO_FACTOR_AUTH action auth
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient With Auth',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [DocumentAccessAuth.TWO_FACTOR_AUTH],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create field for the recipient
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 100,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Try to distribute the envelope - should fail
|
||||||
|
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expect distribution to fail
|
||||||
|
expect(distributeRes.ok()).toBeFalsy();
|
||||||
|
expect(distributeRes.status()).toBe(400);
|
||||||
|
|
||||||
|
const errorResponse = await distributeRes.json();
|
||||||
|
expect(errorResponse.message).toContain('requires an email');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3103,7 +3103,7 @@ test.describe('Document API V2', () => {
|
|||||||
expect(res.status()).toBe(404);
|
expect(res.status()).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow authorized access to envelope use endpoint', async ({ page, request }) => {
|
test('should allow authorized access to envelope use endpoint', async ({ request }) => {
|
||||||
const doc = await seedTemplate({
|
const doc = await seedTemplate({
|
||||||
title: 'Team template 1',
|
title: 'Team template 1',
|
||||||
userId: userA.id,
|
userId: userA.id,
|
||||||
@@ -4313,5 +4313,62 @@ test.describe('Document API V2', () => {
|
|||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Envelope audit logs endpoint', () => {
|
||||||
|
test('should block unauthorized access to envelope audit logs endpoint', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const doc = await seedBlankDocument(userA, teamA.id);
|
||||||
|
|
||||||
|
const res = await request.get(
|
||||||
|
`${WEBAPP_BASE_URL}/api/v2-beta/envelope/${doc.id}/audit-log`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${tokenB}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.ok()).toBeFalsy();
|
||||||
|
expect(res.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow authorized access to envelope audit logs endpoint', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const doc = await seedBlankDocument(userA, teamA.id);
|
||||||
|
|
||||||
|
// Add a recipient which will trigger an audit log.
|
||||||
|
await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: doc.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: 'Test',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request.get(
|
||||||
|
`${WEBAPP_BASE_URL}/api/v2-beta/envelope/${doc.id}/audit-log`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
expect(Array.isArray(data.data)).toBe(true);
|
||||||
|
expect(data.count).toEqual(1);
|
||||||
|
expect(data.data[0].type).toEqual('RECIPIENT_CREATED');
|
||||||
|
expect(data.currentPage).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(data.perPage).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(retrievedFields.length).toBe(3);
|
expect(retrievedFields.length).toBe(3);
|
||||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
expect(retrievedFields.filter((field) => field.type === 'SIGNATURE')).toHaveLength(2);
|
||||||
expect(retrievedFields[1].type).toBe('TEXT');
|
expect(retrievedFields.filter((field) => field.type === 'TEXT')).toHaveLength(1);
|
||||||
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
|
||||||
}).toPass();
|
}).toPass();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,10 +213,8 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(retrievedFields.length).toBe(4);
|
expect(retrievedFields.length).toBe(4);
|
||||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
expect(retrievedFields.filter((field) => field.type === 'SIGNATURE')).toHaveLength(3);
|
||||||
expect(retrievedFields[1].type).toBe('TEXT');
|
expect(retrievedFields.filter((field) => field.type === 'TEXT')).toHaveLength(1);
|
||||||
expect(retrievedFields[2].type).toBe('SIGNATURE');
|
|
||||||
expect(retrievedFields[3].type).toBe('SIGNATURE');
|
|
||||||
}).toPass();
|
}).toPass();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,10 +256,16 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(retrievedFields.length).toBe(2);
|
expect(retrievedFields.length).toBe(2);
|
||||||
expect(retrievedFields[0].type).toBe('SIGNATURE');
|
const textField = retrievedFields.find((field) => field.type === 'TEXT');
|
||||||
expect(retrievedFields[1].type).toBe('TEXT');
|
const signatureField = retrievedFields.find((field) => field.type === 'SIGNATURE');
|
||||||
|
|
||||||
|
expect(signatureField).toBeDefined();
|
||||||
|
expect(textField).toBeDefined();
|
||||||
|
|
||||||
|
if (!signatureField || !textField) {
|
||||||
|
throw new Error('No signature or text field');
|
||||||
|
}
|
||||||
|
|
||||||
const textField = retrievedFields[1];
|
|
||||||
expect(textField.fieldMeta).toBeDefined();
|
expect(textField.fieldMeta).toBeDefined();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { seedUser } from '@documenso/prisma/seed/users';
|
|||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
import { checkDocumentTabCount } from '../fixtures/documents';
|
||||||
|
import { expectToastTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'serial' });
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
@@ -83,14 +84,13 @@ test('[DOCUMENTS]: deleting a completed document should not remove it from recip
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// delete document
|
// delete document
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -128,14 +128,13 @@ test('[DOCUMENTS]: deleting a pending document should remove it from recipients'
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// delete document
|
// delete document
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -169,19 +168,17 @@ test('[DOCUMENTS]: deleting draft documents should permanently remove it', async
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Draft' })
|
.locator('tr', { hasText: 'Document 1 - Draft' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); // Required to reduce flakiness.
|
||||||
|
|
||||||
// delete document
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible();
|
await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await page.waitForTimeout(2500);
|
await expectToastTextToBeVisible(page, 'Document deleted');
|
||||||
|
|
||||||
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible();
|
||||||
|
|
||||||
@@ -203,14 +200,13 @@ test('[DOCUMENTS]: deleting pending documents should permanently remove it', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -239,14 +235,13 @@ test('[DOCUMENTS]: deleting completed documents as an owner should hide it from
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
const documentActionBtn = page
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn');
|
||||||
.click();
|
await openDropdownMenu(page, documentActionBtn);
|
||||||
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@@ -292,36 +287,24 @@ test('[DOCUMENTS]: deleting documents as a recipient should only hide it for the
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await expect(async () => {
|
const completedDocActionBtn = page
|
||||||
await page
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
.getByTestId('document-table-action-btn');
|
||||||
.getByTestId('document-table-action-btn')
|
await openDropdownMenu(page, completedDocActionBtn);
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).waitFor({ state: 'visible' });
|
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
||||||
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
await expect(async () => {
|
const pendingDocActionBtn = page
|
||||||
await page
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.getByTestId('document-table-action-btn');
|
||||||
.getByTestId('document-table-action-btn')
|
await openDropdownMenu(page, pendingDocActionBtn);
|
||||||
.click();
|
|
||||||
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).waitFor({ state: 'visible' });
|
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Hide' }).click({ force: true });
|
||||||
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
await page.getByRole('button', { name: 'Hide' }).click({ force: true });
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
|
||||||
@@ -58,3 +58,15 @@ const getCsrfToken = async (page: Page) => {
|
|||||||
|
|
||||||
return csrfToken;
|
return csrfToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkSessionValid = async (page: Page): Promise<boolean> => {
|
||||||
|
const { request } = page.context();
|
||||||
|
|
||||||
|
const response = await request.fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/session`, {
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await response.json();
|
||||||
|
|
||||||
|
return session.isAuthenticated === true;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Locator } from '@playwright/test';
|
||||||
import { type Page, expect } from '@playwright/test';
|
import { type Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
export const expectTextToBeVisible = async (page: Page, text: string) => {
|
export const expectTextToBeVisible = async (page: Page, text: string) => {
|
||||||
@@ -7,3 +8,22 @@ export const expectTextToBeVisible = async (page: Page, text: string) => {
|
|||||||
export const expectTextToNotBeVisible = async (page: Page, text: string) => {
|
export const expectTextToNotBeVisible = async (page: Page, text: string) => {
|
||||||
await expect(page.getByText(text).first()).not.toBeVisible();
|
await expect(page.getByText(text).first()).not.toBeVisible();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const expectToastTextToBeVisible = async (page: Page, text: string) => {
|
||||||
|
await expect(page.locator('[role="status"]').getByText(text)).toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openDropdownMenu = async (page: Page, dropdownButton: Locator) => {
|
||||||
|
await page.waitForTimeout(500); // Initial timeout incase table remounts which will close the dropdown.
|
||||||
|
await dropdownButton.focus();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await dropdownButton.focus();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem').first()).toBeVisible();
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { seedTeamMember } from '@documenso/prisma/seed/teams';
|
|||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
import { expectTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' });
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
@@ -117,7 +117,9 @@ test('[TEAMS]: can pin a document folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -142,7 +144,9 @@ test('[TEAMS]: can unpin a document folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Unpin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -166,7 +170,9 @@ test('[TEAMS]: can rename a document folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await page.getByLabel('Name').fill('Team Archive');
|
await page.getByLabel('Name').fill('Team Archive');
|
||||||
@@ -191,7 +197,9 @@ test('[TEAMS]: document folder visibility is visible to team member', async ({ p
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible();
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible();
|
||||||
@@ -220,7 +228,9 @@ test('[TEAMS]: document folder can be moved to another document folder', async (
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move' }).click();
|
await page.getByRole('menuitem', { name: 'Move' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Team Clients' }).click();
|
await page.getByRole('button', { name: 'Team Clients' }).click();
|
||||||
@@ -271,7 +281,9 @@ test('[TEAMS]: document folder and its contents can be deleted', async ({ page }
|
|||||||
redirectPath: `/t/${team.url}/documents`,
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
||||||
@@ -280,12 +292,8 @@ test('[TEAMS]: document folder and its contents can be deleted', async ({ page }
|
|||||||
await page.goto(`/t/${team.url}/documents`);
|
await page.goto(`/t/${team.url}/documents`);
|
||||||
|
|
||||||
await expect(page.locator(`[data-folder-id="${folder.id}"]`)).not.toBeVisible();
|
await expect(page.locator(`[data-folder-id="${folder.id}"]`)).not.toBeVisible();
|
||||||
await expect(page.getByText(proposal.title)).not.toBeVisible();
|
await expect(page.getByText(proposal.title)).toBeVisible();
|
||||||
|
await expect(page.getByText(report.title)).toBeVisible();
|
||||||
await page.goto(`/t/${team.url}/documents/f/${folder.id}`);
|
|
||||||
|
|
||||||
await expect(page.getByText(report.title)).not.toBeVisible();
|
|
||||||
await expect(page.locator(`[data-folder-id="${reportsFolder.id}"]`)).not.toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEAMS]: create folder button is visible on templates page', async ({ page }) => {
|
test('[TEAMS]: create folder button is visible on templates page', async ({ page }) => {
|
||||||
@@ -410,7 +418,9 @@ test('[TEAMS]: can pin a template folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
await page.getByRole('menuitem', { name: 'Pin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -436,7 +446,9 @@ test('[TEAMS]: can unpin a template folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Unpin' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
await page.getByRole('menuitem', { name: 'Unpin' }).click();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
@@ -462,7 +474,9 @@ test('[TEAMS]: can rename a template folder', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await page.getByLabel('Name').fill('Updated Team Template Folder');
|
await page.getByLabel('Name').fill('Updated Team Template Folder');
|
||||||
@@ -488,7 +502,9 @@ test('[TEAMS]: template folder visibility is not visible to team member', async
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Visibility' })).not.toBeVisible();
|
||||||
@@ -519,7 +535,9 @@ test('[TEAMS]: template folder can be moved to another template folder', async (
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move' }).click();
|
await page.getByRole('menuitem', { name: 'Move' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Team Client Templates' }).click();
|
await page.getByRole('button', { name: 'Team Client Templates' }).click();
|
||||||
@@ -572,7 +590,9 @@ test('[TEAMS]: template folder can be deleted', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/templates`,
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
await page.getByRole('textbox').fill(`delete ${folder.name}`);
|
||||||
@@ -761,7 +781,9 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/t/${team.url}/documents/`);
|
await page.goto(`/t/${team.url}/documents/`);
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').click();
|
const folderMoreBtn1 = page.getByTestId('folder-card-more-button');
|
||||||
|
await openDropdownMenu(page, folderMoreBtn1);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Admins only');
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Admins only');
|
||||||
@@ -781,7 +803,9 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/t/${team.url}/documents`);
|
await page.goto(`/t/${team.url}/documents`);
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn2 = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn2);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Managers and above');
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Managers and above');
|
||||||
@@ -801,7 +825,9 @@ test('[TEAMS]: folder inherits team visibility settings', async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/t/${team.url}/documents/`);
|
await page.goto(`/t/${team.url}/documents/`);
|
||||||
|
|
||||||
await page.getByTestId('folder-card-more-button').nth(0).click();
|
const folderMoreBtn3 = page.getByTestId('folder-card-more-button').nth(0);
|
||||||
|
await openDropdownMenu(page, folderMoreBtn3);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Settings' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
await page.getByRole('menuitem', { name: 'Settings' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Everyone');
|
await expect(page.getByRole('combobox', { name: 'Visibility' })).toHaveText('Everyone');
|
||||||
@@ -966,7 +992,9 @@ test('[TEAMS]: team member can move documents to everyone folder', async ({ page
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move' }).click();
|
await page.getByRole('menuitem', { name: 'Move' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1018,7 +1046,9 @@ test('[TEAMS]: team manager can move manager document to manager folder', async
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1071,7 +1101,9 @@ test('[TEAMS]: team manager can move manager document to everyone folder', async
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1124,7 +1156,9 @@ test('[TEAMS]: team manager can move everyone document to manager folder', async
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1177,7 +1211,9 @@ test('[TEAMS]: team admin can move admin document to admin folder', async ({ pag
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1228,7 +1264,9 @@ test('[TEAMS]: team admin can move admin document to manager folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1279,7 +1317,9 @@ test('[TEAMS]: team admin can move admin document to everyone folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1330,7 +1370,9 @@ test('[TEAMS]: team admin can move manager document to admin folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1381,7 +1423,9 @@ test('[TEAMS]: team admin can move manager document to manager folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1432,7 +1476,9 @@ test('[TEAMS]: team admin can move manager document to everyone folder', async (
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click({ force: true });
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1483,7 +1529,9 @@ test('[TEAMS]: team admin can move everyone document to admin folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1534,7 +1582,9 @@ test('[TEAMS]: team admin can move everyone document to manager folder', async (
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1585,7 +1635,9 @@ test('[TEAMS]: team admin can move everyone document to everyone folder', async
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1630,7 +1682,9 @@ test('[TEAMS]: team owner can move admin document to admin folder', async ({ pag
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1675,7 +1729,9 @@ test('[TEAMS]: team owner can move admin document to manager folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1720,7 +1776,9 @@ test('[TEAMS]: team owner can move admin document to everyone folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Admin Document')).toBeVisible();
|
||||||
|
|
||||||
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
const adminDocRow = page.getByRole('row', { name: /\[TEST\] Admin Document/ });
|
||||||
await adminDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = adminDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1765,7 +1823,9 @@ test('[TEAMS]: team owner can move manager document to admin folder', async ({ p
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1810,7 +1870,9 @@ test('[TEAMS]: team owner can move manager document to manager folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1855,7 +1917,9 @@ test('[TEAMS]: team owner can move manager document to everyone folder', async (
|
|||||||
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Manager Document')).toBeVisible();
|
||||||
|
|
||||||
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
const managerDocRow = page.getByRole('row', { name: /\[TEST\] Manager Document/ });
|
||||||
await managerDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = managerDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
@@ -1900,7 +1964,9 @@ test('[TEAMS]: team owner can move everyone document to admin folder', async ({
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Admin Folder' })).toBeVisible();
|
||||||
@@ -1945,7 +2011,9 @@ test('[TEAMS]: team owner can move everyone document to manager folder', async (
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Manager Folder' })).toBeVisible();
|
||||||
@@ -1990,7 +2058,9 @@ test('[TEAMS]: team owner can move everyone document to everyone folder', async
|
|||||||
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
await expect(page.getByText('[TEST] Everyone Document')).toBeVisible();
|
||||||
|
|
||||||
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
const everyoneDocRow = page.getByRole('row', { name: /\[TEST\] Everyone Document/ });
|
||||||
await everyoneDocRow.getByTestId('document-table-action-btn').click();
|
const docActionBtn = everyoneDocRow.getByTestId('document-table-action-btn');
|
||||||
|
await openDropdownMenu(page, docActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Everyone Folder' })).toBeVisible();
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations';
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { expectTextToBeVisible, expectTextToNotBeVisible } from '../fixtures/generic';
|
import {
|
||||||
|
expectTextToBeVisible,
|
||||||
|
expectTextToNotBeVisible,
|
||||||
|
openDropdownMenu,
|
||||||
|
} from '../fixtures/generic';
|
||||||
|
|
||||||
test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
|
test('[ORGANISATIONS]: create and delete organisation', async ({ page }) => {
|
||||||
const { user, organisation } = await seedUser({
|
const { user, organisation } = await seedUser({
|
||||||
@@ -399,7 +403,9 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
|
|||||||
await expect(page.getByText('Team members have been added').first()).toBeVisible();
|
await expect(page.getByText('Team members have been added').first()).toBeVisible();
|
||||||
|
|
||||||
// Update CUSTOM_GROUP_B
|
// Update CUSTOM_GROUP_B
|
||||||
await page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button').click();
|
const updateBtn = page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, updateBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Update role' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Update role' }).click();
|
await page.getByRole('menuitem', { name: 'Update role' }).click();
|
||||||
await page.getByRole('combobox').click();
|
await page.getByRole('combobox').click();
|
||||||
await page.getByRole('option', { name: 'Team Admin' }).click();
|
await page.getByRole('option', { name: 'Team Admin' }).click();
|
||||||
@@ -409,7 +415,9 @@ test('[ORGANISATIONS]: manage groups and members', async ({ page }) => {
|
|||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
// Delete CUSTOM_GROUP_B
|
// Delete CUSTOM_GROUP_B
|
||||||
await page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button').click();
|
const deleteBtn = page.getByRole('row', { name: 'CUSTOM_GROUP_B' }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, deleteBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
await expectTextToBeVisible(page, 'You have successfully removed this group from the team.');
|
await expectTextToBeVisible(page, 'You have successfully removed this group from the team.');
|
||||||
@@ -477,7 +485,9 @@ test('[ORGANISATIONS]: member invites', async ({ page }) => {
|
|||||||
await expect(page.getByText(user2.email)).toBeVisible();
|
await expect(page.getByText(user2.email)).toBeVisible();
|
||||||
await expect(page.getByText(user3.email)).toBeVisible();
|
await expect(page.getByText(user3.email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('row', { name: user3.email }).getByRole('button').click();
|
const inviteActionBtn = page.getByRole('row', { name: user3.email }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, inviteActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await expect(page.getByText('Invitation has been deleted').first()).toBeVisible();
|
await expect(page.getByText('Invitation has been deleted').first()).toBeVisible();
|
||||||
await expect(page.getByText(user3.email)).not.toBeVisible();
|
await expect(page.getByText(user3.email)).not.toBeVisible();
|
||||||
@@ -508,7 +518,9 @@ test('[ORGANISATIONS]: member invites', async ({ page }) => {
|
|||||||
await expect(page.getByText(user.email)).toBeVisible();
|
await expect(page.getByText(user.email)).toBeVisible();
|
||||||
await expect(page.getByText(user2.email)).toBeVisible();
|
await expect(page.getByText(user2.email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('row', { name: user2.email }).getByRole('button').click();
|
const memberActionBtn = page.getByRole('row', { name: user2.email }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, memberActionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Remove' }).click();
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
await expect(page.getByText('You have successfully removed').first()).toBeVisible();
|
await expect(page.getByText('You have successfully removed').first()).toBeVisible();
|
||||||
@@ -522,7 +534,9 @@ test('[ORGANISATIONS]: member invites', async ({ page }) => {
|
|||||||
await expect(page.getByText(user.email)).toBeVisible();
|
await expect(page.getByText(user.email)).toBeVisible();
|
||||||
await expect(page.getByText(user2.email)).toBeVisible();
|
await expect(page.getByText(user2.email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('row', { name: user2.email }).getByRole('button').click();
|
const orgMemberBtn = page.getByRole('row', { name: user2.email }).getByRole('button');
|
||||||
|
await openDropdownMenu(page, orgMemberBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
await expect(page.getByText('You have successfully removed this user').first()).toBeVisible();
|
await expect(page.getByText('You have successfully removed this user').first()).toBeVisible();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||||
|
|
||||||
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||||
const { user, team } = await seedUser();
|
const { user, team } = await seedUser();
|
||||||
@@ -44,6 +46,9 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
|||||||
.fill('public-direct-template-description');
|
.fill('public-direct-template-description');
|
||||||
await page.getByRole('button', { name: 'Update' }).click();
|
await page.getByRole('button', { name: 'Update' }).click();
|
||||||
|
|
||||||
|
// Wait for toast
|
||||||
|
await expectToastTextToBeVisible(page, 'Template has been updated');
|
||||||
|
|
||||||
// Check that public profile is disabled.
|
// Check that public profile is disabled.
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||||
await expect(page.locator('body')).toContainText('404 Profile not found');
|
await expect(page.locator('body')).toContainText('404 Profile not found');
|
||||||
@@ -51,7 +56,21 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
|||||||
// Go back to public profile page.
|
// Go back to public profile page.
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
|
||||||
await page.getByRole('switch').click();
|
await page.getByRole('switch').click();
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
// Expect profile to be enabled via db.
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const profile = await prisma.teamProfile.findFirst({
|
||||||
|
where: { teamId: team.id },
|
||||||
|
});
|
||||||
|
return profile?.enabled;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 1000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBeTruthy();
|
||||||
|
|
||||||
// Assert values.
|
// Assert values.
|
||||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import { seedUser } from '@documenso/prisma/seed/users';
|
|||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { checkDocumentTabCount } from '../fixtures/documents';
|
import { checkDocumentTabCount } from '../fixtures/documents';
|
||||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
import {
|
||||||
|
expectTextToBeVisible,
|
||||||
|
expectToastTextToBeVisible,
|
||||||
|
openDropdownMenu,
|
||||||
|
} from '../fixtures/generic';
|
||||||
|
|
||||||
test('[TEAMS]: check team documents count', async ({ page }) => {
|
test('[TEAMS]: check team documents count', async ({ page }) => {
|
||||||
const { team, teamOwner, teamMember2 } = await seedTeamDocuments();
|
const { team, teamOwner, teamMember2 } = await seedTeamDocuments();
|
||||||
@@ -239,21 +243,15 @@ test('[TEAMS]: resend pending team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeAttached();
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await page.waitForTimeout(1000);
|
await expect(page.getByRole('menuitem', { name: 'Resend' })).toBeVisible();
|
||||||
|
await page.getByRole('menuitem', { name: 'Resend' }).click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Resend' })).toBeVisible();
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem').filter({ hasText: 'Resend' }).click();
|
|
||||||
await page.getByLabel('test.documenso.com').first().click();
|
await page.getByLabel('test.documenso.com').first().click();
|
||||||
await page.getByRole('button', { name: 'Send reminder' }).click();
|
await page.getByRole('button', { name: 'Send reminder' }).click();
|
||||||
|
|
||||||
await expect(
|
await expectToastTextToBeVisible(page, 'Document re-sent');
|
||||||
page.getByRole('status').filter({ hasText: 'Document re-sent' }).first(),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
||||||
@@ -265,14 +263,12 @@ test('[TEAMS]: delete draft team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeVisible({
|
||||||
|
timeout: 500,
|
||||||
await page.waitForTimeout(1000);
|
});
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
@@ -309,14 +305,12 @@ test('[TEAMS]: delete pending team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeVisible({
|
||||||
|
timeout: 500,
|
||||||
await page.waitForTimeout(1000);
|
});
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
||||||
@@ -354,14 +348,12 @@ test('[TEAMS]: delete completed team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(async () => {
|
const actionBtn = page.getByTestId('document-table-action-btn').first();
|
||||||
await page.getByTestId('document-table-action-btn').first().click();
|
await expect(actionBtn).toBeVisible({
|
||||||
|
timeout: 500,
|
||||||
await page.waitForTimeout(1000);
|
});
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('menuitem', { name: 'Delete' }).click({ force: true });
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
await page.getByRole('button', { name: 'Delete' }).click({ force: true });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { seedTeamEmailVerification } from '@documenso/prisma/seed/teams';
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test('[TEAMS]: send team email request', async ({ page }) => {
|
test('[TEAMS]: send team email request', async ({ page }) => {
|
||||||
const { user, team } = await seedUser();
|
const { user, team } = await seedUser();
|
||||||
@@ -54,8 +55,13 @@ test('[TEAMS]: delete team email', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/settings`,
|
redirectPath: `/t/${team.url}/settings`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.locator('section div').filter({ hasText: 'Team email' }).getByRole('button').click();
|
const settingsBtn = page
|
||||||
|
.locator('section div')
|
||||||
|
.filter({ hasText: 'Team email' })
|
||||||
|
.getByRole('button');
|
||||||
|
await openDropdownMenu(page, settingsBtn);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Remove' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||||
await page.getByRole('button', { name: 'Remove' }).click();
|
await page.getByRole('button', { name: 'Remove' }).click();
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,13 @@ test.describe('AutoSave Fields Step', () => {
|
|||||||
['SIGNATURE', 'TEXT'].toSorted(),
|
['SIGNATURE', 'TEXT'].toSorted(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const textField = fields[1];
|
const textField = fields.find((field) => field.type === 'TEXT');
|
||||||
|
expect(textField).toBeDefined();
|
||||||
|
|
||||||
|
if (!textField) {
|
||||||
|
throw new Error('No text field');
|
||||||
|
}
|
||||||
|
|
||||||
expect(textField.fieldMeta).toBeDefined();
|
expect(textField.fieldMeta).toBeDefined();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams';
|
|||||||
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
test('[TEMPLATES]: view templates', async ({ page }) => {
|
test('[TEMPLATES]: view templates', async ({ page }) => {
|
||||||
const { team, owner, organisation } = await seedTeam({
|
const { team, owner, organisation } = await seedTeam({
|
||||||
@@ -71,13 +72,14 @@ test('[TEMPLATES]: delete template', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const template of ['Team template 1', 'Team template 2']) {
|
for (const template of ['Team template 1', 'Team template 2']) {
|
||||||
await page
|
const actionBtn = page
|
||||||
.getByRole('row', { name: template })
|
.getByRole('row', { name: template })
|
||||||
.getByRole('cell', { name: 'Use Template' })
|
.getByRole('cell', { name: 'Use Template' })
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
.nth(1)
|
.nth(1);
|
||||||
.click();
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
await expect(page.getByText('Template deleted').first()).toBeVisible();
|
await expect(page.getByText('Template deleted').first()).toBeVisible();
|
||||||
@@ -110,7 +112,9 @@ test('[TEMPLATES]: duplicate template', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Duplicate team template.
|
// Duplicate team template.
|
||||||
await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click();
|
const actionBtn = page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1);
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Duplicate' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||||
await page.getByRole('button', { name: 'Duplicate' }).click();
|
await page.getByRole('button', { name: 'Duplicate' }).click();
|
||||||
await expect(page.getByText('Template duplicated').first()).toBeVisible();
|
await expect(page.getByText('Template duplicated').first()).toBeVisible();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type Page, expect, test } from '@playwright/test';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout, checkSessionValid } from '../fixtures/authentication';
|
||||||
|
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
|
|||||||
|
|
||||||
await page.goto('http://localhost:3000/signin');
|
await page.goto('http://localhost:3000/signin');
|
||||||
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/forgot-password');
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Email' }).click();
|
await page.getByRole('textbox', { name: 'Email' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
||||||
@@ -24,7 +25,9 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
|
|||||||
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
|
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
|
||||||
await page.getByRole('button', { name: 'Reset Password' }).click();
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
|
||||||
await expect(page.locator('body')).toContainText('Reset email sent', { timeout: 10000 });
|
await expect(page.locator('body')).toContainText('Reset email sent', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -109,3 +112,116 @@ test('[USER] can reset password via user settings', async ({ page }: { page: Pag
|
|||||||
await page.waitForURL('/settings/profile');
|
await page.waitForURL('/settings/profile');
|
||||||
await expect(page).toHaveURL('/settings/profile');
|
await expect(page).toHaveURL('/settings/profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[USER] password reset invalidates all sessions', async ({ page }: { page: Page }) => {
|
||||||
|
const oldPassword = 'Test123!';
|
||||||
|
const newPassword = 'Test124!';
|
||||||
|
|
||||||
|
const { user } = await seedUser({
|
||||||
|
password: oldPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
const initialCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/signin');
|
||||||
|
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/forgot-password');
|
||||||
|
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
||||||
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Reset email sent', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`http://localhost:3000/reset-password/${foundToken.token}`);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(newPassword);
|
||||||
|
await page.getByLabel('Repeat Password').fill(newPassword);
|
||||||
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Your password has been updated successfully.');
|
||||||
|
|
||||||
|
await page.context().addCookies(initialCookies);
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/settings/profile');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/signin');
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(false);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: newPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/settings/profile');
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[USER] password update invalidates other sessions but keeps current', async ({
|
||||||
|
page,
|
||||||
|
}: {
|
||||||
|
page: Page;
|
||||||
|
}) => {
|
||||||
|
const oldPassword = 'Test123!';
|
||||||
|
const newPassword = 'Test124!';
|
||||||
|
|
||||||
|
const { user } = await seedUser({
|
||||||
|
password: oldPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
const initialCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/settings/security');
|
||||||
|
await page.getByLabel('Current password').fill(oldPassword);
|
||||||
|
await page.getByLabel('New password').fill(newPassword);
|
||||||
|
await page.getByLabel('Repeat password').fill(newPassword);
|
||||||
|
await page.getByRole('button', { name: 'Update password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Password updated');
|
||||||
|
|
||||||
|
const finalCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.context().addCookies(initialCookies);
|
||||||
|
await page.goto('http://localhost:3000/settings/profile');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/signin');
|
||||||
|
expect(await checkSessionValid(page)).toBe(false);
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.context().addCookies(finalCookies);
|
||||||
|
await page.goto('http://localhost:3000/settings/security');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/settings/security');
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
|||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||||
import { expectTextToBeVisible } from '../fixtures/generic';
|
import { expectTextToBeVisible, openDropdownMenu } from '../fixtures/generic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to seed a webhook directly in the database for testing.
|
* Helper function to seed a webhook directly in the database for testing.
|
||||||
@@ -147,9 +147,11 @@ test('[WEBHOOKS]: delete webhook', async ({ page }) => {
|
|||||||
|
|
||||||
// Find the row with the webhook and click the action dropdown
|
// Find the row with the webhook and click the action dropdown
|
||||||
const webhookRow = page.locator('tr', { hasText: webhookUrl });
|
const webhookRow = page.locator('tr', { hasText: webhookUrl });
|
||||||
await webhookRow.getByTestId('webhook-table-action-btn').click();
|
const actionBtn = webhookRow.getByTestId('webhook-table-action-btn');
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
|
||||||
// Click Delete menu item
|
// Click Delete menu item
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
// Fill in confirmation field
|
// Fill in confirmation field
|
||||||
@@ -196,9 +198,11 @@ test('[WEBHOOKS]: update webhook', async ({ page }) => {
|
|||||||
|
|
||||||
// Find the row with the webhook and click the action dropdown
|
// Find the row with the webhook and click the action dropdown
|
||||||
const webhookRow = page.locator('tr', { hasText: originalWebhookUrl });
|
const webhookRow = page.locator('tr', { hasText: originalWebhookUrl });
|
||||||
await webhookRow.getByTestId('webhook-table-action-btn').click();
|
const actionBtn = webhookRow.getByTestId('webhook-table-action-btn');
|
||||||
|
await openDropdownMenu(page, actionBtn);
|
||||||
|
|
||||||
// Click Edit menu item
|
// Click Edit menu item
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Edit' })).toBeVisible();
|
||||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
|
||||||
// Wait for dialog to open
|
// Wait for dialog to open
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
|
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
|
||||||
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
|
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
|
||||||
"test:e2e": "NODE_OPTIONS=--experimental-require-module NODE_ENV=test start-server-and-test \"npm run start -w @documenso/remix\" http://localhost:3000 \"playwright test $E2E_TEST_PATH\""
|
"test:e2e": "NODE_OPTIONS=--experimental-require-module NODE_ENV=test NEXT_PRIVATE_LOGGER_FILE_PATH=./logs.json start-server-and-test \"npm run start -w @documenso/remix\" http://localhost:3000 \"playwright test $E2E_TEST_PATH\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
function calculateWorkers() {
|
||||||
|
const total = os.cpus().length;
|
||||||
|
|
||||||
|
// Reserve 2 cores for the system
|
||||||
|
const usable = Math.max(total - 2, 1);
|
||||||
|
|
||||||
|
// 1 worker per 2 cores, minimum 1
|
||||||
|
const workers = Math.max(Math.floor(usable / 2), 1);
|
||||||
|
|
||||||
|
// Max 6 workers
|
||||||
|
return Math.min(workers, 6);
|
||||||
|
}
|
||||||
|
|
||||||
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
|
const ENV_FILES = ['.env', '.env.local', `.env.${process.env.NODE_ENV || 'development'}`];
|
||||||
|
|
||||||
ENV_FILES.forEach((file) => {
|
ENV_FILES.forEach((file) => {
|
||||||
@@ -15,9 +29,8 @@ ENV_FILES.forEach((file) => {
|
|||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
/* Run tests in files in parallel */
|
fullyParallel: true,
|
||||||
fullyParallel: false,
|
workers: 10, // See Projects where 10 is utilized for API tests. We're not running 10 workers for UI tests.
|
||||||
workers: 2,
|
|
||||||
maxFailures: process.env.CI ? 1 : undefined,
|
maxFailures: process.env.CI ? 1 : undefined,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
@@ -31,25 +44,54 @@ export default defineConfig({
|
|||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on',
|
trace: 'retain-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
video: 'on-first-retry',
|
|
||||||
|
|
||||||
/* Add explicit timeouts for actions */
|
/* Add explicit timeouts for actions */
|
||||||
actionTimeout: 15_000,
|
actionTimeout: 15_000,
|
||||||
navigationTimeout: 30_000,
|
navigationTimeout: 30_000,
|
||||||
|
|
||||||
|
contextOptions: {
|
||||||
|
reducedMotion: 'reduce',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Disable animations via cookie for more stable tests */
|
||||||
|
storageState: {
|
||||||
|
cookies: [
|
||||||
|
{
|
||||||
|
name: '__disable_animations',
|
||||||
|
value: 'true',
|
||||||
|
domain: 'localhost',
|
||||||
|
path: '/',
|
||||||
|
expires: -1,
|
||||||
|
httpOnly: false,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'Lax' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
origins: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
|
// API Tests e2e/api/**/*.spec.ts
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'api',
|
||||||
|
testMatch: /e2e\/api\/.*\.spec\.ts/,
|
||||||
|
workers: 10, // Limited by DB connections before it gets flakey.
|
||||||
|
},
|
||||||
|
// Run UI Tests
|
||||||
|
{
|
||||||
|
name: 'ui',
|
||||||
|
testMatch: /e2e\/(?!api\/).*\.spec\.ts/,
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Chrome'],
|
...devices['Desktop Chrome'],
|
||||||
viewport: { width: 1920, height: 1200 },
|
viewport: { width: 1920, height: 1200 },
|
||||||
},
|
},
|
||||||
|
workers: calculateWorkers(),
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { env } from '@documenso/lib/utils/env';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||||
|
import { invalidateSessions } from '../lib/session/session';
|
||||||
import { getCsrfCookie } from '../lib/session/session-cookies';
|
import { getCsrfCookie } from '../lib/session/session-cookies';
|
||||||
import { onAuthorize } from '../lib/utils/authorizer';
|
import { onAuthorize } from '../lib/utils/authorizer';
|
||||||
import { getSession } from '../lib/utils/get-session';
|
import { getSession } from '../lib/utils/get-session';
|
||||||
@@ -170,15 +171,38 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
const { password, currentPassword } = c.req.valid('json');
|
const { password, currentPassword } = c.req.valid('json');
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
const session = await getSession(c);
|
const { session, user } = await getSession(c);
|
||||||
|
|
||||||
await updatePassword({
|
await updatePassword({
|
||||||
userId: session.user.id,
|
userId: user.id,
|
||||||
password,
|
password,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userSessionIds = await prisma.session
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id satisfies number, // Incase we pass undefined somehow.
|
||||||
|
id: {
|
||||||
|
not: session.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((sessions) => sessions.map((s) => s.id));
|
||||||
|
|
||||||
|
if (userSessionIds.length > 0) {
|
||||||
|
await invalidateSessions({
|
||||||
|
userId: user.id,
|
||||||
|
sessionIds: userSessionIds,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
isRevoke: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@@ -231,12 +255,33 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
|
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
await resetPassword({
|
const { userId } = await resetPassword({
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate all sessions after successful password reset
|
||||||
|
const userSessionIds = await prisma.session
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: userId satisfies number, // Incase we pass undefined somehow.
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((sessions) => sessions.map((session) => session.id));
|
||||||
|
|
||||||
|
if (userSessionIds.length > 0) {
|
||||||
|
await invalidateSessions({
|
||||||
|
userId,
|
||||||
|
sessionIds: userSessionIds,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
isRevoke: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,17 +32,17 @@
|
|||||||
"@react-email/img": "0.0.11",
|
"@react-email/img": "0.0.11",
|
||||||
"@react-email/link": "0.0.12",
|
"@react-email/link": "0.0.12",
|
||||||
"@react-email/preview": "0.0.13",
|
"@react-email/preview": "0.0.13",
|
||||||
"@react-email/render": "0.0.17",
|
"@react-email/render": "2.0.0",
|
||||||
"@react-email/row": "0.0.12",
|
"@react-email/row": "0.0.12",
|
||||||
"@react-email/section": "0.0.16",
|
"@react-email/section": "0.0.16",
|
||||||
"@react-email/tailwind": "^2.0.1",
|
"@react-email/tailwind": "^2.0.1",
|
||||||
"@react-email/text": "0.1.5",
|
"@react-email/text": "0.1.5",
|
||||||
"nodemailer": "^7.0.10",
|
"nodemailer": "^7.0.10",
|
||||||
"react-email": "^5.0.4",
|
"react-email": "^5.0.6",
|
||||||
"resend": "^6.5.2"
|
"resend": "^6.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tsconfig": "*",
|
"@documenso/tsconfig": "*",
|
||||||
"@types/nodemailer": "^6.4.21"
|
"@types/nodemailer": "^7.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type RenderOptions = ReactEmail.Options & {
|
|||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
const colors = (config.theme?.extend?.colors || {}) as Record<string, string>;
|
const colors = (config.theme?.extend?.colors || {}) as Record<string, string>;
|
||||||
|
|
||||||
export const render = (element: React.ReactNode, options?: RenderOptions) => {
|
export const render = async (element: React.ReactNode, options?: RenderOptions) => {
|
||||||
const { branding, ...otherOptions } = options ?? {};
|
const { branding, ...otherOptions } = options ?? {};
|
||||||
|
|
||||||
return ReactEmail.render(
|
return ReactEmail.render(
|
||||||
@@ -36,7 +36,7 @@ export const render = (element: React.ReactNode, options?: RenderOptions) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderWithI18N = (element: React.ReactNode, options?: RenderOptions) => {
|
export const renderWithI18N = async (element: React.ReactNode, options?: RenderOptions) => {
|
||||||
const { branding, i18n, ...otherOptions } = options ?? {};
|
const { branding, i18n, ...otherOptions } = options ?? {};
|
||||||
|
|
||||||
if (!i18n) {
|
if (!i18n) {
|
||||||
@@ -62,24 +62,3 @@ export const renderWithI18N = (element: React.ReactNode, options?: RenderOptions
|
|||||||
otherOptions,
|
otherOptions,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderAsync = async (element: React.ReactNode, options?: RenderOptions) => {
|
|
||||||
const { branding, ...otherOptions } = options ?? {};
|
|
||||||
|
|
||||||
return await ReactEmail.renderAsync(
|
|
||||||
<BrandingProvider branding={branding}>
|
|
||||||
<Tailwind
|
|
||||||
config={{
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{element}
|
|
||||||
</Tailwind>
|
|
||||||
</BrandingProvider>,
|
|
||||||
otherOptions,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/cli
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -77,7 +78,8 @@ export const run = async ({
|
|||||||
const recipientsToNotify = envelope.recipients.filter(
|
const recipientsToNotify = envelope.recipients.filter(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
||||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
);
|
);
|
||||||
|
|
||||||
await io.runTask('send-cancellation-emails', async () => {
|
await io.runTask('send-cancellation-emails', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
|||||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
import type { JobRunIO } from '../../client/_internal/job';
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||||
@@ -79,8 +80,8 @@ export const run = async ({
|
|||||||
|
|
||||||
const recipientReference = recipientName || recipientEmail;
|
const recipientReference = recipientName || recipientEmail;
|
||||||
|
|
||||||
// Don't send notification if the owner is the one who signed
|
// Don't send notification if the owner is the one who signed.
|
||||||
if (owner.email === recipientEmail) {
|
if (owner.email === recipientEmail || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||||
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -85,36 +86,38 @@ export const run = async ({
|
|||||||
const i18n = await getI18nInstance(emailLanguage);
|
const i18n = await getI18nInstance(emailLanguage);
|
||||||
|
|
||||||
// Send confirmation email to the recipient who rejected
|
// Send confirmation email to the recipient who rejected
|
||||||
await io.runTask('send-rejection-confirmation-email', async () => {
|
if (isRecipientEmailValidForSending(recipient)) {
|
||||||
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
await io.runTask('send-rejection-confirmation-email', async () => {
|
||||||
recipientName: recipient.name,
|
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
||||||
documentName: envelope.title,
|
recipientName: recipient.name,
|
||||||
documentOwnerName: envelope.user.name || envelope.user.email,
|
documentName: envelope.title,
|
||||||
reason: recipient.rejectionReason || '',
|
documentOwnerName: envelope.user.name || envelope.user.email,
|
||||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
reason: recipient.rejectionReason || '',
|
||||||
});
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
|
});
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
const [html, text] = await Promise.all([
|
||||||
renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
|
renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
|
||||||
renderEmailWithI18N(recipientTemplate, {
|
renderEmailWithI18N(recipientTemplate, {
|
||||||
lang: emailLanguage,
|
lang: emailLanguage,
|
||||||
branding,
|
branding,
|
||||||
plainText: true,
|
plainText: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
address: recipient.email,
|
address: recipient.email,
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
from: senderEmail,
|
||||||
replyTo: replyToEmail,
|
replyTo: replyToEmail,
|
||||||
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Send notification email to document owner
|
// Send notification email to document owner
|
||||||
await io.runTask('send-owner-notification-email', async () => {
|
await io.runTask('send-owner-notification-email', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -177,31 +178,33 @@ export const run = async ({
|
|||||||
includeSenderDetails: settings.includeSenderDetails,
|
includeSenderDetails: settings.includeSenderDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
await io.runTask('send-signing-email', async () => {
|
if (isRecipientEmailValidForSending(recipient)) {
|
||||||
const [html, text] = await Promise.all([
|
await io.runTask('send-signing-email', async () => {
|
||||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
const [html, text] = await Promise.all([
|
||||||
renderEmailWithI18N(template, {
|
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||||
lang: emailLanguage,
|
renderEmailWithI18N(template, {
|
||||||
branding,
|
lang: emailLanguage,
|
||||||
plainText: true,
|
branding,
|
||||||
}),
|
plainText: true,
|
||||||
]);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
address: recipient.email,
|
address: recipient.email,
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
from: senderEmail,
|
||||||
replyTo: replyToEmail,
|
replyTo: replyToEmail,
|
||||||
subject: renderCustomEmailTemplate(
|
subject: renderCustomEmailTemplate(
|
||||||
documentMeta?.subject || emailSubject,
|
documentMeta?.subject || emailSubject,
|
||||||
customEmailTemplate,
|
customEmailTemplate,
|
||||||
),
|
),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
await io.runTask('update-recipient', async () => {
|
await io.runTask('update-recipient', async () => {
|
||||||
await prisma.recipient.update({
|
await prisma.recipient.update({
|
||||||
|
|||||||
@@ -92,9 +92,23 @@ export const run = async ({
|
|||||||
teamId: envelope.teamId,
|
teamId: envelope.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure all CC recipients are marked as signed
|
||||||
|
await prisma.recipient.updateMany({
|
||||||
|
where: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
role: RecipientRole.CC,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const isComplete =
|
const isComplete =
|
||||||
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
|
||||||
envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED);
|
envelope.recipients.every(
|
||||||
|
(recipient) =>
|
||||||
|
recipient.signingStatus === SigningStatus.SIGNED || recipient.role === RecipientRole.CC,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isComplete) {
|
if (!isComplete) {
|
||||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EnvelopeType } from '@prisma/client';
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -69,6 +70,12 @@ export const send2FATokenEmail = async ({ token, envelopeId }: Send2FATokenEmail
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Recipient is missing email address',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
||||||
envelopeId,
|
envelopeId,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
|
|
||||||
@@ -64,14 +65,18 @@ export const adminSuperDeleteDocument = async ({
|
|||||||
envelope.documentMeta,
|
envelope.documentMeta,
|
||||||
).documentDeleted;
|
).documentDeleted;
|
||||||
|
|
||||||
|
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
|
);
|
||||||
|
|
||||||
// if the document is pending, send cancellation emails to all recipients
|
// if the document is pending, send cancellation emails to all recipients
|
||||||
if (
|
if (
|
||||||
status === DocumentStatus.PENDING &&
|
status === DocumentStatus.PENDING &&
|
||||||
envelope.recipients.length > 0 &&
|
recipientsToNotify.length > 0 &&
|
||||||
isDocumentDeletedEmailEnabled
|
isDocumentDeletedEmailEnabled
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { createCanvas, loadImage } from '@napi-rs/canvas';
|
|||||||
import { DocumentStatus, type Field, RecipientRole } from '@prisma/client';
|
import { DocumentStatus, type Field, RecipientRole } from '@prisma/client';
|
||||||
import { generateObject } from 'ai';
|
import { generateObject } from 'ai';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../../../errors/app-error';
|
||||||
import { getFileServerSide } from '../../../../universal/upload/get-file.server';
|
import { getFileServerSide } from '../../../../universal/upload/get-file.server';
|
||||||
|
import { resizeImageToGeminiImage } from '../../../../utils/images/resize-image-to-gemini-image';
|
||||||
import { getEnvelopeById } from '../../../envelope/get-envelope-by-id';
|
import { getEnvelopeById } from '../../../envelope/get-envelope-by-id';
|
||||||
import { createEnvelopeRecipients } from '../../../recipient/create-envelope-recipients';
|
import { createEnvelopeRecipients } from '../../../recipient/create-envelope-recipients';
|
||||||
import { vertex } from '../../google';
|
import { vertex } from '../../google';
|
||||||
@@ -238,21 +238,6 @@ const maskFieldsOnImage = async ({ image, width, height, fields }: MaskFieldsOnI
|
|||||||
return canvas.encode('jpeg');
|
return canvas.encode('jpeg');
|
||||||
};
|
};
|
||||||
|
|
||||||
const TARGET_SIZE = 1000;
|
|
||||||
|
|
||||||
type ResizeImageOptions = {
|
|
||||||
image: Buffer;
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize image to 1000x1000 using fill strategy.
|
|
||||||
* Scales to cover the target area and crops any overflow.
|
|
||||||
*/
|
|
||||||
const resizeImageToSquare = async ({ image, size = TARGET_SIZE }: ResizeImageOptions) => {
|
|
||||||
return await sharp(image).resize(size, size, { fit: 'fill' }).toBuffer();
|
|
||||||
};
|
|
||||||
|
|
||||||
type DetectFieldsFromPageOptions = {
|
type DetectFieldsFromPageOptions = {
|
||||||
image: Buffer;
|
image: Buffer;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
@@ -267,7 +252,7 @@ const detectFieldsFromPage = async ({
|
|||||||
context,
|
context,
|
||||||
}: DetectFieldsFromPageOptions) => {
|
}: DetectFieldsFromPageOptions) => {
|
||||||
// Resize to 1000x1000 for consistent coordinate mapping
|
// Resize to 1000x1000 for consistent coordinate mapping
|
||||||
const resizedImage = await resizeImageToSquare({ image });
|
const resizedImage = await resizeImageToGeminiImage({ image });
|
||||||
|
|
||||||
// Build messages array
|
// Build messages array
|
||||||
const messages: Parameters<typeof generateObject>[0]['messages'] = [
|
const messages: Parameters<typeof generateObject>[0]['messages'] = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
EnvelopeType,
|
EnvelopeType,
|
||||||
|
FieldType,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
@@ -43,6 +44,14 @@ export type CompleteDocumentWithTokenOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Override the recipient information. This will only work if the recipient
|
||||||
|
* does not have a name or email set.
|
||||||
|
*/
|
||||||
|
recipientOverride?: {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocumentWithToken = async ({
|
export const completeDocumentWithToken = async ({
|
||||||
@@ -52,6 +61,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
nextSigner,
|
nextSigner,
|
||||||
|
recipientOverride,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -116,6 +126,35 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let recipientName = recipient.name;
|
||||||
|
let recipientEmail = recipient.email;
|
||||||
|
|
||||||
|
// Only trim the name if it's been derived.
|
||||||
|
if (!recipientName) {
|
||||||
|
recipientName = (
|
||||||
|
recipientOverride?.name ||
|
||||||
|
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trim the email if it's been derived.
|
||||||
|
if (!recipient.email) {
|
||||||
|
recipientEmail = (
|
||||||
|
recipientOverride?.email ||
|
||||||
|
fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipientEmail) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Recipient email is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check ACCESS AUTH 2FA validation during document completion
|
// Check ACCESS AUTH 2FA validation during document completion
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
@@ -129,6 +168,12 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!recipient.email.trim()) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: `Recipient ${recipient.id} requires an email because they have auth requirements.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isValid = await isRecipientAuthorized({
|
const isValid = await isRecipientAuthorized({
|
||||||
type: 'ACCESS_2FA',
|
type: 'ACCESS_2FA',
|
||||||
documentAuthOptions: envelope.authOptions,
|
documentAuthOptions: envelope.authOptions,
|
||||||
@@ -176,9 +221,43 @@ export const completeDocumentWithToken = async ({
|
|||||||
data: {
|
data: {
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
signedAt: new Date(),
|
signedAt: new Date(),
|
||||||
|
name: recipientName,
|
||||||
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (recipientEmail !== recipient.email || recipientName !== recipient.name) {
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
user: {
|
||||||
|
name: recipientName,
|
||||||
|
email: recipientEmail,
|
||||||
|
},
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: RECIPIENT_DIFF_TYPE.NAME,
|
||||||
|
from: recipient.name,
|
||||||
|
to: recipientName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
||||||
|
from: recipient.email,
|
||||||
|
to: recipientEmail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const authOptions = extractDocumentAuthMethods({
|
const authOptions = extractDocumentAuthMethods({
|
||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
@@ -189,13 +268,13 @@ export const completeDocumentWithToken = async ({
|
|||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient.name,
|
name: recipientName,
|
||||||
email: recipient.email,
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipientEmail,
|
||||||
recipientName: recipient.name,
|
recipientName: recipientName,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
actionAuth: authOptions.derivedRecipientActionAuth,
|
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||||
@@ -247,8 +326,8 @@ export const completeDocumentWithToken = async ({
|
|||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient.name,
|
name: recipientName,
|
||||||
email: recipient.email,
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { ApiRequestMetadata } from '../../universal/extract-request-metadat
|
|||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getMemberRoles } from '../team/get-member-roles';
|
import { getMemberRoles } from '../team/get-member-roles';
|
||||||
@@ -209,7 +210,7 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
// Send cancellation emails to recipients.
|
// Send cancellation emails to recipients.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
envelope.recipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
@@ -118,7 +119,7 @@ export const resendDocument = async ({
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
recipientsToRemind.map(async (recipient) => {
|
recipientsToRemind.map(async (recipient) => {
|
||||||
if (recipient.role === RecipientRole.CC) {
|
if (recipient.role === RecipientRole.CC || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { formatDocumentsPath } from '../../utils/teams';
|
import { formatDocumentsPath } from '../../utils/teams';
|
||||||
@@ -176,8 +177,12 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
'signer.name': recipient.name,
|
'signer.name': recipient.name,
|
||||||
'signer.email': recipient.email,
|
'signer.email': recipient.email,
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ import {
|
|||||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
@@ -128,6 +130,24 @@ export const sendDocument = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that recipients with auth requirements have a valid email.
|
||||||
|
envelope.recipients.forEach((recipient) => {
|
||||||
|
const auth = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
recipient.role !== RecipientRole.CC &&
|
||||||
|
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) &&
|
||||||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: `Recipient ${recipient.id} requires an email because they have auth requirements.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
||||||
// decide if we want to enforce this for API & templates.
|
// decide if we want to enforce this for API & templates.
|
||||||
// const fields = await getFieldsForDocument({
|
// const fields = await getFieldsForDocument({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
|
|
||||||
@@ -69,6 +70,11 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
|
|||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
|
// Skip sending email if recipient has no email address
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
const template = createElement(DocumentPendingEmailTemplate, {
|
const template = createElement(DocumentPendingEmailTemplate, {
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export type CreateEmbeddingPresignTokenOptions = {
|
|||||||
* In development mode, can be set to 0 to create a token that expires immediately (for testing)
|
* In development mode, can be set to 0 to create a token that expires immediately (for testing)
|
||||||
*/
|
*/
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
|
scope?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createEmbeddingPresignToken = async ({
|
export const createEmbeddingPresignToken = async ({
|
||||||
apiToken,
|
apiToken,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
scope,
|
||||||
}: CreateEmbeddingPresignTokenOptions) => {
|
}: CreateEmbeddingPresignTokenOptions) => {
|
||||||
try {
|
try {
|
||||||
// Validate the API token
|
// Validate the API token
|
||||||
@@ -40,6 +42,7 @@ export const createEmbeddingPresignToken = async ({
|
|||||||
const token = await new SignJWT({
|
const token = await new SignJWT({
|
||||||
aud: String(validatedToken.teamId ?? validatedToken.userId),
|
aud: String(validatedToken.teamId ?? validatedToken.userId),
|
||||||
sub: String(validatedToken.id),
|
sub: String(validatedToken.id),
|
||||||
|
scope,
|
||||||
})
|
})
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
.setIssuedAt(now.toJSDate())
|
.setIssuedAt(now.toJSDate())
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import { AppError, AppErrorCode } from '../../errors/app-error';
|
|||||||
|
|
||||||
export type VerifyEmbeddingPresignTokenOptions = {
|
export type VerifyEmbeddingPresignTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
|
scope?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyEmbeddingPresignToken = async ({
|
export const verifyEmbeddingPresignToken = async ({
|
||||||
token,
|
token,
|
||||||
|
scope,
|
||||||
}: VerifyEmbeddingPresignTokenOptions) => {
|
}: VerifyEmbeddingPresignTokenOptions) => {
|
||||||
// First decode the JWT to get the claims without verification
|
// First decode the JWT to get the claims without verification
|
||||||
let decodedToken: JWTPayload;
|
let decodedToken: JWTPayload;
|
||||||
@@ -81,6 +83,12 @@ export const verifyEmbeddingPresignToken = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decodedToken.scope && scope && decodedToken.scope !== scope) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'Presign token scope not matched',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Now verify the token with the actual secret
|
// Now verify the token with the actual secret
|
||||||
const secret = new TextEncoder().encode(apiToken.token);
|
const secret = new TextEncoder().encode(apiToken.token);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { PDFDocument } from '@cantoo/pdf-lib';
|
|||||||
import { TextAlignment, rgb, setFontAndSize } from '@cantoo/pdf-lib';
|
import { TextAlignment, rgb, setFontAndSize } from '@cantoo/pdf-lib';
|
||||||
import fontkit from '@pdf-lib/fontkit';
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
import { getPageSize } from './get-page-size';
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +16,7 @@ export async function addRejectionStampToPdf(
|
|||||||
const pages = pdfDoc.getPages();
|
const pages = pdfDoc.getPages();
|
||||||
pdfDoc.registerFontkit(fontkit);
|
pdfDoc.registerFontkit(fontkit);
|
||||||
|
|
||||||
const fontBytes = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
const fontBytes = await fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||||
async (res) => res.arrayBuffer(),
|
async (res) => res.arrayBuffer(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '@cantoo/pdf-lib';
|
} from '@cantoo/pdf-lib';
|
||||||
import fontkit from '@pdf-lib/fontkit';
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
|
|
||||||
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
||||||
const context = document.context;
|
const context = document.context;
|
||||||
@@ -29,7 +29,7 @@ export const flattenForm = async (document: PDFDocument) => {
|
|||||||
|
|
||||||
const form = document.getForm();
|
const form = document.getForm();
|
||||||
|
|
||||||
const fontNoto = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
const fontNoto = await fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||||
async (res) => res.arrayBuffer(),
|
async (res) => res.arrayBuffer(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
|||||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDateFieldMeta,
|
ZDateFieldMeta,
|
||||||
@@ -37,8 +37,12 @@ import { getPageSize } from './get-page-size';
|
|||||||
|
|
||||||
export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const insertFieldInPDFV1 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const [fontCaveat, fontNoto] = await Promise.all([
|
const [fontCaveat, fontNoto] = await Promise.all([
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) =>
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isSignatureField = isSignatureFieldType(field.type);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
|
|||||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||||
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PRIVATE_INTERNAL_WEBAPP_URL } from '../../constants/app';
|
||||||
import {
|
import {
|
||||||
ZCheckboxFieldMeta,
|
ZCheckboxFieldMeta,
|
||||||
ZDateFieldMeta,
|
ZDateFieldMeta,
|
||||||
@@ -30,8 +30,12 @@ import { getPageSize } from './get-page-size';
|
|||||||
|
|
||||||
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
export const legacy_insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
||||||
const [fontCaveat, fontNoto] = await Promise.all([
|
const [fontCaveat, fontNoto] = await Promise.all([
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) =>
|
||||||
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
fetch(`${NEXT_PRIVATE_INTERNAL_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) =>
|
||||||
|
res.arrayBuffer(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isSignatureField = isSignatureFieldType(field.type);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { loadAvatar } from '../../utils/images/avatar';
|
||||||
|
|
||||||
export type GetAvatarImageOptions = {
|
export type GetAvatarImageOptions = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@@ -17,10 +17,5 @@ export const getAvatarImage = async ({ id }: GetAvatarImageOptions) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytes = Buffer.from(avatarImage.bytes, 'base64');
|
return await loadAvatar(avatarImage.bytes);
|
||||||
|
|
||||||
return {
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
content: await sharp(bytes).toFormat('jpeg').toBuffer(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
|
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
|
||||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||||
import { AppError } from '../../errors/app-error';
|
import { AppError } from '../../errors/app-error';
|
||||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
|
import { optimiseAvatar } from '../../utils/images/avatar';
|
||||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
|
|
||||||
@@ -100,10 +99,7 @@ export const setAvatarImage = async ({
|
|||||||
let newAvatarImageId: string | null = null;
|
let newAvatarImageId: string | null = null;
|
||||||
|
|
||||||
if (bytes) {
|
if (bytes) {
|
||||||
const optimisedBytes = await sharp(Buffer.from(bytes, 'base64'))
|
const optimisedBytes = await optimiseAvatar(bytes);
|
||||||
.resize(512, 512)
|
|
||||||
.toFormat('jpeg', { quality: 75 })
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const avatarImage = await prisma.avatarImage.create({
|
const avatarImage = await prisma.avatarImage.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
@@ -142,7 +142,8 @@ export const deleteEnvelopeRecipient = async ({
|
|||||||
if (
|
if (
|
||||||
recipientToDelete.sendStatus === SendStatus.SENT &&
|
recipientToDelete.sendStatus === SendStatus.SENT &&
|
||||||
isRecipientRemovedEmailEnabled &&
|
isRecipientRemovedEmailEnabled &&
|
||||||
envelope.type === EnvelopeType.DOCUMENT
|
envelope.type === EnvelopeType.DOCUMENT &&
|
||||||
|
isRecipientEmailValidForSending(recipientToDelete)
|
||||||
) {
|
) {
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
@@ -294,10 +294,15 @@ export const setDocumentRecipients = async ({
|
|||||||
envelope.documentMeta,
|
envelope.documentMeta,
|
||||||
).recipientRemoved;
|
).recipientRemoved;
|
||||||
|
|
||||||
// Send emails to deleted recipients.
|
// Send emails to deleted recipients who have emails.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
removedRecipients.map(async (recipient) => {
|
removedRecipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientRemovedEmailEnabled) {
|
if (
|
||||||
|
recipient.sendStatus !== SendStatus.SENT ||
|
||||||
|
recipient.role === RecipientRole.CC ||
|
||||||
|
!isRecipientRemovedEmailEnabled ||
|
||||||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,4 +85,8 @@ export const resetPassword = async ({ token, password, requestMetadata }: ResetP
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: foundToken.userId,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1368
-1007
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
||||||
@@ -110,3 +111,13 @@ export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
|
|||||||
documentId: true,
|
documentId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZRecipientEmailSchema = z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.email({ message: msg`Invalid email`.id })
|
||||||
|
.max(254),
|
||||||
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
export const optimiseAvatar = async (bytes: string) => {
|
||||||
|
return await sharp(Buffer.from(bytes, 'base64'))
|
||||||
|
.resize(512, 512)
|
||||||
|
.toFormat('jpeg', { quality: 75 })
|
||||||
|
.toBuffer();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAvatar = async (bytes: string) => {
|
||||||
|
const content = await sharp(Buffer.from(bytes, 'base64')).toFormat('jpeg').toBuffer();
|
||||||
|
return {
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
export const loadLogo = async (file: Uint8Array) => {
|
||||||
|
const content = await sharp(file).toFormat('png', { quality: 80 }).toBuffer();
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentType: 'image/png',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
export const TARGET_SIZE = 1000;
|
||||||
|
|
||||||
|
type ResizeImageToGeminiImageOptions = {
|
||||||
|
image: Buffer;
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize image to 1000x1000 using fill strategy.
|
||||||
|
* Scales to cover the target area and crops any overflow.
|
||||||
|
*/
|
||||||
|
export const resizeImageToGeminiImage = async ({
|
||||||
|
image,
|
||||||
|
size = TARGET_SIZE,
|
||||||
|
}: ResizeImageToGeminiImageOptions) => {
|
||||||
|
return await sharp(image).resize(size, size, { fit: 'fill' }).toBuffer();
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
export const svgToPng = async (svg: string) => {
|
||||||
|
return await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Envelope } from '@prisma/client';
|
import type { Envelope } from '@prisma/client';
|
||||||
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||||
import { extractLegacyIds } from '../universal/id';
|
import { extractLegacyIds } from '../universal/id';
|
||||||
@@ -58,3 +59,7 @@ export const mapRecipientToLegacyRecipient = (
|
|||||||
...legacyId,
|
...legacyId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isRecipientEmailValidForSending = (recipient: Pick<Recipient, 'email'>) => {
|
||||||
|
return z.string().email().safeParse(recipient.email).success;
|
||||||
|
};
|
||||||
|
|||||||
@@ -587,6 +587,9 @@ export const seedPendingDocumentWithFullFields = async ({
|
|||||||
where: {
|
where: {
|
||||||
envelopeId: document.id,
|
envelopeId: document.id,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
signingOrder: 'asc',
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
fields: true,
|
fields: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const createEmbeddingPresignTokenRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { expiresIn } = input;
|
const { expiresIn, scope } = input;
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED()) {
|
if (IS_BILLING_ENABLED()) {
|
||||||
const token = await getApiTokenByToken({ token: apiToken });
|
const token = await getApiTokenByToken({ token: apiToken });
|
||||||
@@ -54,6 +54,7 @@ export const createEmbeddingPresignTokenRoute = procedure
|
|||||||
const presignToken = await createEmbeddingPresignToken({
|
const presignToken = await createEmbeddingPresignToken({
|
||||||
apiToken,
|
apiToken,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
scope,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...presignToken };
|
return { ...presignToken };
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export const ZCreateEmbeddingPresignTokenRequestSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default(60)
|
.default(60)
|
||||||
.describe('Expiration time in minutes (default: 60, max: 10,080)'),
|
.describe('Expiration time in minutes (default: 60, max: 10,080)'),
|
||||||
|
scope: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Resource restriction. Example: documentId:1, templateId:2'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
|
export const ZCreateEmbeddingPresignTokenResponseSchema = z.object({
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ZFieldWidthSchema,
|
ZFieldWidthSchema,
|
||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
|
|||||||
documentDataId: z.string(),
|
documentDataId: z.string(),
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.union([z.string().length(0), z.string().email()]),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export const updateEmbeddingDocumentRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
const apiToken = await verifyEmbeddingPresignToken({
|
||||||
|
token: presignToken,
|
||||||
|
scope: `documentId:${input.documentId}`,
|
||||||
|
});
|
||||||
|
|
||||||
const { documentId, title, externalId, recipients, meta } = input;
|
const { documentId, title, externalId, recipients, meta } = input;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ export const updateEmbeddingTemplateRoute = procedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
const apiToken = await verifyEmbeddingPresignToken({
|
||||||
|
token: presignToken,
|
||||||
|
scope: `templateId:${input.templateId}`,
|
||||||
|
});
|
||||||
|
|
||||||
const { templateId, title, externalId, recipients, meta } = input;
|
const { templateId, title, externalId, recipients, meta } = input;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
@@ -21,22 +21,11 @@ import {
|
|||||||
ZFieldPageYSchema,
|
ZFieldPageYSchema,
|
||||||
ZFieldWidthSchema,
|
ZFieldWidthSchema,
|
||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||||
|
|
||||||
const ZFieldSchema = z.object({
|
|
||||||
id: z.number().optional(),
|
|
||||||
type: z.nativeEnum(FieldType),
|
|
||||||
pageNumber: ZFieldPageNumberSchema,
|
|
||||||
pageX: ZFieldPageXSchema,
|
|
||||||
pageY: ZFieldPageYSchema,
|
|
||||||
width: ZFieldWidthSchema,
|
|
||||||
height: ZFieldHeightSchema,
|
|
||||||
fieldMeta: ZFieldMetaSchema.optional(),
|
|
||||||
envelopeItemId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
@@ -44,7 +33,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z.union([z.string().length(0), z.string().email()]),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ export const verifyEmbeddingPresignTokenRoute = procedure
|
|||||||
.output(ZVerifyEmbeddingPresignTokenResponseSchema)
|
.output(ZVerifyEmbeddingPresignTokenResponseSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const { token } = input;
|
const { token, scope } = input;
|
||||||
|
|
||||||
const apiToken = await verifyEmbeddingPresignToken({
|
const apiToken = await verifyEmbeddingPresignToken({
|
||||||
token,
|
token,
|
||||||
|
scope,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
return { success: !!apiToken };
|
return { success: !!apiToken };
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user