Compare commits
11 Commits
v1.9.0-rc.
...
v1.9.1-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00b46561c2 | ||
|
|
11bc93a9a4 | ||
|
|
11528090a5 | ||
|
|
3c4863f285 | ||
|
|
2ff330f9d4 | ||
|
|
ce1c93b2a6 | ||
|
|
82337e4e3a | ||
|
|
7d9a3f9776 | ||
|
|
cbad065dac | ||
|
|
25a3861c91 | ||
|
|
b9ae277041 |
@@ -111,6 +111,83 @@ The colors will be automatically converted to the appropriate format internally.
|
|||||||
|
|
||||||
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
|
4. **Consistent Radius**: Use a consistent border radius value that matches your application's design system.
|
||||||
|
|
||||||
|
## CSS Class Targets
|
||||||
|
|
||||||
|
In addition to CSS variables, specific components in the embedded experience can be targeted using CSS classes for more granular styling:
|
||||||
|
|
||||||
|
### Component Classes
|
||||||
|
|
||||||
|
| Class Name | Description |
|
||||||
|
| --------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| `.embed--Root` | Main container for the embedded signing experience |
|
||||||
|
| `.embed--DocumentContainer` | Container for the document and signing widget |
|
||||||
|
| `.embed--DocumentViewer` | Container for the document viewer |
|
||||||
|
| `.embed--DocumentWidget` | The signing widget container |
|
||||||
|
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget, handles positioning |
|
||||||
|
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
|
||||||
|
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
|
||||||
|
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
|
||||||
|
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
|
||||||
|
| `.embed--WaitingForTurn` | Container for the waiting screen when it's not the user's turn to sign |
|
||||||
|
| `.embed--DocumentCompleted` | Container for the completion screen after signing |
|
||||||
|
| `.field--FieldRootContainer` | Base container for document fields (signatures, text, checkboxes, etc.) |
|
||||||
|
|
||||||
|
Field components also expose several data attributes that can be used for styling different states:
|
||||||
|
|
||||||
|
| Data Attribute | Values | Description |
|
||||||
|
| ------------------- | ---------------------------------------------- | ------------------------------------ |
|
||||||
|
| `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field |
|
||||||
|
| `[data-inserted]` | `true`, `false` | Whether the field has been filled |
|
||||||
|
| `[data-validate]` | `true`, `false` | Whether the field is being validated |
|
||||||
|
|
||||||
|
### Field Styling Example
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Style all field containers */
|
||||||
|
.field--FieldRootContainer {
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style specific field types */
|
||||||
|
.field--FieldRootContainer[data-field-type='SIGNATURE'] {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style inserted fields */
|
||||||
|
.field--FieldRootContainer[data-inserted='true'] {
|
||||||
|
background-color: var(--primary);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style fields being validated */
|
||||||
|
.field--FieldRootContainer[data-validate='true'] {
|
||||||
|
border-color: orange;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Custom styles for the document widget */
|
||||||
|
.embed--DocumentWidget {
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom styles for the waiting screen */
|
||||||
|
.embed--WaitingForTurn {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for the document container */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.embed--DocumentContainer {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [React Integration](/developers/embedding/react)
|
- [React Integration](/developers/embedding/react)
|
||||||
|
|||||||
@@ -85,12 +85,13 @@ You can also set the recipient's role, which determines their actions and permis
|
|||||||
|
|
||||||
Documenso has 4 roles for recipients with different permissions and actions.
|
Documenso has 4 roles for recipients with different permissions and actions.
|
||||||
|
|
||||||
| Role | Function | Action required | Signature |
|
| Role | Function | Action required | Signature |
|
||||||
| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
||||||
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
||||||
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
||||||
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
||||||
| BCC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
| Assistant | Can help prepare the document by filling in fields on behalf of other signers. | Yes | No |
|
||||||
|
| CC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.12",
|
"version": "1.9.1-rc.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
field={field}
|
field={field}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
|
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
@@ -342,6 +343,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSignature(value);
|
setSignature(value);
|
||||||
}}
|
}}
|
||||||
|
allowTypedSignature={template.templateMeta?.typedSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -179,14 +179,8 @@ export const NumberField = ({ field, onSignField, onUnsignField }: NumberFieldPr
|
|||||||
|
|
||||||
const onRemove = async () => {
|
const onRemove = async () => {
|
||||||
try {
|
try {
|
||||||
if (isAssistantMode && !targetSigner) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
|
||||||
|
|
||||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||||
token: signingRecipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,26 +68,16 @@ export const RadioField = ({ field, onSignField, onUnsignField }: RadioFieldProp
|
|||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
try {
|
try {
|
||||||
if (isAssistantMode && !targetSigner) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
|
||||||
|
|
||||||
const payload: TSignFieldWithTokenMutationSchema = {
|
const payload: TSignFieldWithTokenMutationSchema = {
|
||||||
token: signingRecipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: selectedOption,
|
value: selectedOption,
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
authOptions,
|
authOptions,
|
||||||
...(isAssistantMode && {
|
|
||||||
isAssistantPrefill: true,
|
|
||||||
assistantId: recipient.id,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onSignField) {
|
if (onSignField) {
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
|
|||||||
export interface RejectDocumentDialogProps {
|
export interface RejectDocumentDialogProps {
|
||||||
document: Pick<Document, 'id'>;
|
document: Pick<Document, 'id'>;
|
||||||
token: string;
|
token: string;
|
||||||
|
onRejected?: (reason: string) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
|
export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr
|
|||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
router.push(`/sign/${token}/rejected`);
|
if (onRejected) {
|
||||||
|
await onRejected(reason);
|
||||||
|
} else {
|
||||||
|
router.push(`/sign/${token}/rejected`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|||||||
@@ -179,7 +179,13 @@ export const SigningPageView = ({
|
|||||||
)
|
)
|
||||||
.map((field) =>
|
.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => <SignatureField key={field.id} field={field} />)
|
.with(FieldType.SIGNATURE, () => (
|
||||||
|
<SignatureField
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
|
||||||
|
/>
|
||||||
|
))
|
||||||
.with(FieldType.INITIALS, () => <InitialsField key={field.id} field={field} />)
|
.with(FieldType.INITIALS, () => <InitialsField key={field.id} field={field} />)
|
||||||
.with(FieldType.NAME, () => <NameField key={field.id} field={field} />)
|
.with(FieldType.NAME, () => <NameField key={field.id} field={field} />)
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type EmbedDocumentCompletedPageProps = {
|
|||||||
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
||||||
console.log({ signature });
|
console.log({ signature });
|
||||||
return (
|
return (
|
||||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
<h3 className="text-foreground text-2xl font-semibold">
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
<Trans>Document Completed!</Trans>
|
<Trans>Document Completed!</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
40
apps/web/src/app/embed/rejected.tsx
Normal file
40
apps/web/src/app/embed/rejected.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { XCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { Signature } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type EmbedDocumentRejectedPageProps = {
|
||||||
|
name?: string;
|
||||||
|
signature?: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => {
|
||||||
|
return (
|
||||||
|
<div className="embed--DocumentRejected relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<XCircle className="text-destructive h-10 w-10" />
|
||||||
|
|
||||||
|
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||||
|
<Trans>Document Rejected</Trans>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-destructive mt-4 flex items-center text-center text-sm">
|
||||||
|
<Trans>You have rejected this document</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
|
||||||
|
<Trans>
|
||||||
|
The document owner has been notified of your decision. They may contact you with further
|
||||||
|
instructions if necessary.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
|
||||||
|
<Trans>No further action is required from you at this time.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,7 +10,13 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import {
|
||||||
|
type DocumentData,
|
||||||
|
type Field,
|
||||||
|
FieldType,
|
||||||
|
RecipientRole,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
@@ -26,11 +32,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||||
|
import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { EmbedClientLoading } from '../../client-loading';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { EmbedDocumentCompleted } from '../../completed';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedDocumentFields } from '../../document-fields';
|
||||||
|
import { EmbedDocumentRejected } from '../../rejected';
|
||||||
import { injectCss } from '../../util';
|
import { injectCss } from '../../util';
|
||||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||||
|
|
||||||
@@ -75,6 +83,9 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||||
|
const [hasRejectedDocument, setHasRejectedDocument] = useState(
|
||||||
|
recipient.signingStatus === SigningStatus.REJECTED,
|
||||||
|
);
|
||||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||||
allRecipients.length > 0 ? allRecipients[0].id : null,
|
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||||
);
|
);
|
||||||
@@ -83,6 +94,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
|
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||||
|
|
||||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||||
|
|
||||||
@@ -161,6 +174,25 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDocumentRejected = (reason: string) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-rejected',
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
documentId,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRejectedDocument(true);
|
||||||
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const hash = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
@@ -174,6 +206,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
// Since a recipient can be provided a name we can lock it without requiring
|
// Since a recipient can be provided a name we can lock it without requiring
|
||||||
// a to be provided by the parent application, unlike direct templates.
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
setIsNameLocked(!!data.lockName);
|
setIsNameLocked(!!data.lockName);
|
||||||
|
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
||||||
|
|
||||||
if (data.darkModeDisabled) {
|
if (data.darkModeDisabled) {
|
||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
@@ -208,6 +241,10 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
|
if (hasRejectedDocument) {
|
||||||
|
return <EmbedDocumentRejected name={fullName} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasCompletedDocument) {
|
if (hasCompletedDocument) {
|
||||||
return (
|
return (
|
||||||
<EmbedDocumentCompleted
|
<EmbedDocumentCompleted
|
||||||
@@ -226,12 +263,22 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
|
<RecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
|
||||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
{allowDocumentRejection && (
|
||||||
|
<div className="embed--Actions mb-4 flex w-full flex-row-reverse items-baseline justify-between">
|
||||||
|
<RejectDocumentDialog
|
||||||
|
document={{ id: documentId }}
|
||||||
|
token={token}
|
||||||
|
onRejected={onDocumentRejected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="flex-1">
|
<div className="embed--DocumentViewer flex-1">
|
||||||
<LazyPDFViewer
|
<LazyPDFViewer
|
||||||
documentData={documentData}
|
documentData={documentData}
|
||||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
@@ -241,12 +288,12 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div className="embed--DocumentWidgetHeader">
|
||||||
<div className="flex items-center justify-between gap-x-2">
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||||
{isAssistantMode ? (
|
{isAssistantMode ? (
|
||||||
@@ -272,7 +319,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
{isAssistantMode ? (
|
{isAssistantMode ? (
|
||||||
<Trans>Help complete the document for other signers.</Trans>
|
<Trans>Help complete the document for other signers.</Trans>
|
||||||
@@ -285,7 +332,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
<div className="embed--DocumentWidgetForm -mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
{isAssistantMode && (
|
{isAssistantMode && (
|
||||||
<div>
|
<div>
|
||||||
@@ -413,14 +460,14 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
|
|
||||||
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||||
|
|
||||||
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
<div className="embed--DocumentWidgetFooter mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||||
{pendingFields.length > 0 ? (
|
{pendingFields.length > 0 ? (
|
||||||
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
|
||||||
<Trans>Next</Trans>
|
<Trans>Next</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className="col-start-2"
|
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
|
||||||
disabled={
|
disabled={
|
||||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
|||||||
.optional()
|
.optional()
|
||||||
.transform((value) => value || undefined),
|
.transform((value) => value || undefined),
|
||||||
lockName: z.boolean().optional().default(false),
|
lockName: z.boolean().optional().default(false),
|
||||||
|
allowDocumentRejection: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const EmbedWaitingForTurn = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="embed--WaitingForTurn relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
<h3 className="text-foreground text-center text-2xl font-bold">
|
<h3 className="text-foreground text-center text-2xl font-bold">
|
||||||
<Trans>Waiting for Your Turn</Trans>
|
<Trans>Waiting for Your Turn</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.0-rc.12",
|
"version": "1.9.1-rc.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.0-rc.12",
|
"version": "1.9.1-rc.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.12",
|
"version": "1.9.1-rc.2",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.0-rc.12",
|
"version": "1.9.1-rc.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { jobs } from '../../jobs/client';
|
import { jobs } from '../../jobs/client';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
import {
|
import {
|
||||||
@@ -72,6 +73,13 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Recipient has already rejected the document',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Account" ADD COLUMN "password" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Session" DROP COLUMN "expires",
|
||||||
|
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
ADD COLUMN "ipAddress" TEXT,
|
||||||
|
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
ADD COLUMN "userAgent" TEXT;
|
||||||
@@ -270,18 +270,25 @@ model Account {
|
|||||||
scope String?
|
scope String?
|
||||||
id_token String? @db.Text
|
id_token String? @db.Text
|
||||||
session_state String?
|
session_state String?
|
||||||
|
password String?
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
@@unique([provider, providerAccountId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
sessionToken String @unique
|
sessionToken String @unique
|
||||||
userId Int
|
userId Int
|
||||||
expires DateTime
|
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
ipAddress String?
|
||||||
|
userAgent String?
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DocumentStatus {
|
enum DocumentStatus {
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
|||||||
|
|
||||||
import { prisma } from '..';
|
import { prisma } from '..';
|
||||||
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
||||||
|
import { seedPendingDocument } from './documents';
|
||||||
|
import { seedDirectTemplate, seedTemplate } from './templates';
|
||||||
|
|
||||||
|
const createDocumentData = async ({ documentData }: { documentData: string }) => {
|
||||||
|
return prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: documentData,
|
||||||
|
initialData: documentData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const seedDatabase = async () => {
|
export const seedDatabase = async () => {
|
||||||
const examplePdf = fs
|
const examplePdf = fs
|
||||||
@@ -39,35 +51,80 @@ export const seedDatabase = async () => {
|
|||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const examplePdfData = await prisma.documentData.upsert({
|
for (let i = 1; i <= 4; i++) {
|
||||||
where: {
|
const documentData = await createDocumentData({ documentData: examplePdf });
|
||||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
data: examplePdf,
|
|
||||||
initialData: examplePdf,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.document.create({
|
await prisma.document.create({
|
||||||
data: {
|
data: {
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
title: 'Example Document',
|
title: `Example Document ${i}`,
|
||||||
documentDataId: examplePdfData.id,
|
documentDataId: documentData.id,
|
||||||
userId: exampleUser.id,
|
userId: exampleUser.id,
|
||||||
recipients: {
|
recipients: {
|
||||||
create: {
|
create: {
|
||||||
name: String(adminUser.name),
|
name: String(adminUser.name),
|
||||||
email: adminUser.email,
|
email: adminUser.email,
|
||||||
token: Math.random().toString(36).slice(2, 9),
|
token: Math.random().toString(36).slice(2, 9),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
const documentData = await createDocumentData({ documentData: examplePdf });
|
||||||
|
|
||||||
|
await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
source: DocumentSource.DOCUMENT,
|
||||||
|
title: `Document ${i}`,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: adminUser.id,
|
||||||
|
recipients: {
|
||||||
|
create: {
|
||||||
|
name: String(exampleUser.name),
|
||||||
|
email: exampleUser.email,
|
||||||
|
token: Math.random().toString(36).slice(2, 9),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await seedPendingDocument(exampleUser, [adminUser], {
|
||||||
|
key: 'example-pending',
|
||||||
|
createDocumentOptions: {
|
||||||
|
title: 'Pending Document',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await seedPendingDocument(adminUser, [exampleUser], {
|
||||||
|
key: 'admin-pending',
|
||||||
|
createDocumentOptions: {
|
||||||
|
title: 'Pending Document',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
seedTemplate({
|
||||||
|
title: 'Template 1',
|
||||||
|
userId: exampleUser.id,
|
||||||
|
}),
|
||||||
|
seedDirectTemplate({
|
||||||
|
title: 'Direct Template 1',
|
||||||
|
userId: exampleUser.id,
|
||||||
|
}),
|
||||||
|
|
||||||
|
seedTemplate({
|
||||||
|
title: 'Template 1',
|
||||||
|
userId: adminUser.id,
|
||||||
|
}),
|
||||||
|
seedDirectTemplate({
|
||||||
|
title: 'Direct Template 1',
|
||||||
|
userId: adminUser.id,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const testUsers = [
|
const testUsers = [
|
||||||
'test@documenso.com',
|
'test@documenso.com',
|
||||||
'test2@documenso.com',
|
'test2@documenso.com',
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const getCardClassNames = (
|
|||||||
checkBoxOrRadio: boolean,
|
checkBoxOrRadio: boolean,
|
||||||
cardClassName?: string,
|
cardClassName?: string,
|
||||||
) => {
|
) => {
|
||||||
const baseClasses = 'field-card-container relative z-20 h-full w-full transition-all';
|
const baseClasses =
|
||||||
|
'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all';
|
||||||
|
|
||||||
const insertedClasses =
|
const insertedClasses =
|
||||||
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
|
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
|
||||||
@@ -141,6 +142,7 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont
|
|||||||
<Card
|
<Card
|
||||||
id={`field-${field.id}`}
|
id={`field-${field.id}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
data-field-type={field.type}
|
||||||
data-inserted={field.inserted ? 'true' : 'false'}
|
data-inserted={field.inserted ? 'true' : 'false'}
|
||||||
className={cardClassNames}
|
className={cardClassNames}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user