mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 19:21:39 +10:00
chore: improve logic
This commit is contained in:
@ -15,11 +15,11 @@ import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
SplitButton,
|
||||
SplitButtonAction,
|
||||
SplitButtonDropdown,
|
||||
SplitButtonDropdownItem,
|
||||
} from '@documenso/ui/primitives/split-button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DocumentPageViewButtonProps = {
|
||||
@ -31,7 +31,9 @@ export type DocumentPageViewButtonProps = {
|
||||
team?: Pick<Team, 'id' | 'url'>;
|
||||
};
|
||||
|
||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
||||
export const DocumentPageViewButton = ({
|
||||
document: activeDocument,
|
||||
}: DocumentPageViewButtonProps) => {
|
||||
const { data: session } = useSession();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
@ -40,33 +42,27 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
||||
return null;
|
||||
}
|
||||
|
||||
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
|
||||
const recipient = activeDocument.recipients.find(
|
||||
(recipient) => recipient.email === session.user.email,
|
||||
);
|
||||
|
||||
const isRecipient = !!recipient;
|
||||
const isPending = document.status === DocumentStatus.PENDING;
|
||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||
const isPending = activeDocument.status === DocumentStatus.PENDING;
|
||||
const isComplete = activeDocument.status === DocumentStatus.COMPLETED;
|
||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const role = recipient?.role;
|
||||
|
||||
const documentsPath = formatDocumentsPath(document.team?.url);
|
||||
const documentsPath = formatDocumentsPath(activeDocument.team?.url);
|
||||
|
||||
const onDownloadClick = async ({
|
||||
includeCertificate = true,
|
||||
includeAuditLog = true,
|
||||
}: {
|
||||
includeCertificate?: boolean;
|
||||
includeAuditLog?: boolean;
|
||||
} = {}) => {
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
{
|
||||
documentId: document.id,
|
||||
includeCertificate,
|
||||
includeAuditLog,
|
||||
documentId: activeDocument.id,
|
||||
},
|
||||
{
|
||||
context: {
|
||||
teamId: document.team?.id?.toString(),
|
||||
teamId: activeDocument.team?.id?.toString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -80,8 +76,6 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
||||
await downloadPDF({
|
||||
documentData,
|
||||
fileName: documentWithData.title,
|
||||
includeCertificate,
|
||||
includeAuditLog,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
@ -92,6 +86,100 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadAuditLogClick = async () => {
|
||||
try {
|
||||
const { url } = await trpcClient.document.downloadAuditLogs.mutate({
|
||||
documentId: activeDocument.id,
|
||||
});
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
});
|
||||
|
||||
Object.assign(iframe.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '0',
|
||||
height: '0',
|
||||
});
|
||||
|
||||
const onLoaded = () => {
|
||||
if (iframe.contentDocument?.readyState === 'complete') {
|
||||
iframe.contentWindow?.print();
|
||||
|
||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
||||
iframe.addEventListener('load', onLoaded);
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
onLoaded();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadSigningCertificateClick = async () => {
|
||||
try {
|
||||
const { url } = await trpcClient.document.downloadCertificate.mutate({
|
||||
documentId: activeDocument.id,
|
||||
});
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
});
|
||||
|
||||
Object.assign(iframe.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '0',
|
||||
height: '0',
|
||||
});
|
||||
|
||||
const onLoaded = () => {
|
||||
if (iframe.contentDocument?.readyState === 'complete') {
|
||||
iframe.contentWindow?.print();
|
||||
|
||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
||||
iframe.addEventListener('load', onLoaded);
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
onLoaded();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`Sorry, we were unable to download the certificate. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return match({
|
||||
isRecipient,
|
||||
isPending,
|
||||
@ -125,50 +213,27 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
||||
))
|
||||
.with({ isComplete: false }, () => (
|
||||
<Button className="w-full" asChild>
|
||||
<Link href={`${documentsPath}/${document.id}/edit`}>
|
||||
<Link href={`${documentsPath}/${activeDocument.id}/edit`}>
|
||||
<Trans>Edit</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: true }, () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="w-full">
|
||||
<SplitButton className="flex w-full">
|
||||
<SplitButtonAction className="w-full" onClick={() => void onDownloadClick()}>
|
||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</SplitButtonAction>
|
||||
<SplitButtonDropdown>
|
||||
<SplitButtonDropdownItem onClick={() => void onDownloadAuditLogClick()}>
|
||||
<Trans>Only Audit Log</Trans>
|
||||
</SplitButtonDropdownItem>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onClick={() => void onDownloadClick()}>
|
||||
<Trans>Complete Document</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
void onDownloadClick({ includeCertificate: true, includeAuditLog: false })
|
||||
}
|
||||
>
|
||||
<Trans>Without Audit Log</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
void onDownloadClick({ includeCertificate: false, includeAuditLog: true })
|
||||
}
|
||||
>
|
||||
<Trans>Without Certificate</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
void onDownloadClick({ includeCertificate: false, includeAuditLog: false })
|
||||
}
|
||||
>
|
||||
<Trans>Without Certificate & Audit Log</Trans>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<SplitButtonDropdownItem onClick={() => void onDownloadSigningCertificateClick()}>
|
||||
<Trans>Only Signing Certificate</Trans>
|
||||
</SplitButtonDropdownItem>
|
||||
</SplitButtonDropdown>
|
||||
</SplitButton>
|
||||
))
|
||||
.otherwise(() => null);
|
||||
};
|
||||
|
||||
@ -187,6 +187,8 @@ export const EditDocumentForm = ({
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
includeSigningCertificate: data.includeSigningCertificate,
|
||||
includeAuditTrailLog: data.includeAuditTrailLog,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
},
|
||||
|
||||
@ -363,6 +363,40 @@ export const DocumentHistorySheet = ({
|
||||
]}
|
||||
/>
|
||||
))
|
||||
.with(
|
||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED },
|
||||
({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
{
|
||||
key: 'Old',
|
||||
value: data.from,
|
||||
},
|
||||
{
|
||||
key: 'New',
|
||||
value: data.to,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
)
|
||||
.with(
|
||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED },
|
||||
({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
{
|
||||
key: 'Old',
|
||||
value: data.from,
|
||||
},
|
||||
{
|
||||
key: 'New',
|
||||
value: data.to,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
)
|
||||
.exhaustive()}
|
||||
|
||||
{isUserDetailsVisible && (
|
||||
|
||||
@ -6,16 +6,9 @@ import { downloadFile } from './download-file';
|
||||
type DownloadPDFProps = {
|
||||
documentData: DocumentData;
|
||||
fileName?: string;
|
||||
includeCertificate?: boolean;
|
||||
includeAuditLog?: boolean;
|
||||
};
|
||||
|
||||
export const downloadPDF = async ({
|
||||
documentData,
|
||||
fileName,
|
||||
includeCertificate,
|
||||
includeAuditLog,
|
||||
}: DownloadPDFProps) => {
|
||||
export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) => {
|
||||
const bytes = await getFile(documentData);
|
||||
|
||||
const blob = new Blob([bytes], {
|
||||
@ -24,18 +17,8 @@ export const downloadPDF = async ({
|
||||
|
||||
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
|
||||
|
||||
let suffix = '_signed';
|
||||
|
||||
if (includeCertificate && includeAuditLog) {
|
||||
suffix = suffix + '_with_certificate_and_audit';
|
||||
} else if (includeCertificate) {
|
||||
suffix = suffix + '_with_certificate';
|
||||
} else if (includeAuditLog) {
|
||||
suffix = suffix + '_with_audit';
|
||||
}
|
||||
|
||||
downloadFile({
|
||||
filename: `${baseTitle}${suffix}.pdf`,
|
||||
filename: `${baseTitle}_signed.pdf`,
|
||||
data: blob,
|
||||
});
|
||||
};
|
||||
|
||||
@ -124,6 +124,8 @@ export const createDocument = async ({
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
includeSigningCertificate: team?.teamGlobalSettings?.includeSigningCertificate ?? true,
|
||||
includeAuditTrailLog: team?.teamGlobalSettings?.includeAuditTrailLog ?? true,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
|
||||
@ -111,16 +111,31 @@ export const sealDocument = async ({
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFile(documentData);
|
||||
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
let includeSigningCertificate;
|
||||
|
||||
if (document.teamId) {
|
||||
includeSigningCertificate =
|
||||
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
||||
} else {
|
||||
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
||||
}
|
||||
|
||||
const certificateData = includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const auditLogData =
|
||||
(document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true)
|
||||
let includeAuditTrailLog;
|
||||
|
||||
if (document.teamId) {
|
||||
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
||||
} else {
|
||||
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
||||
}
|
||||
|
||||
const auditLogData = includeAuditTrailLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
|
||||
@ -21,6 +21,8 @@ export type UpdateDocumentOptions = {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
includeSigningCertificate?: boolean;
|
||||
includeAuditTrailLog?: boolean;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
@ -156,6 +158,12 @@ export const updateDocument = async ({
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
const isIncludeSigningCertificateSame =
|
||||
data.includeSigningCertificate === undefined ||
|
||||
data.includeSigningCertificate === document.includeSigningCertificate;
|
||||
const isIncludeAuditTrailLogSame =
|
||||
data.includeAuditTrailLog === undefined ||
|
||||
data.includeAuditTrailLog === document.includeAuditTrailLog;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
@ -235,6 +243,34 @@ export const updateDocument = async ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isIncludeSigningCertificateSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: String(document.includeSigningCertificate),
|
||||
to: String(data.includeSigningCertificate || false),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isIncludeAuditTrailLogSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: String(document.includeAuditTrailLog),
|
||||
to: String(data.includeAuditTrailLog || false),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
@ -254,6 +290,8 @@ export const updateDocument = async ({
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
includeSigningCertificate: data.includeSigningCertificate,
|
||||
includeAuditTrailLog: data.includeAuditTrailLog,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
@ -29,7 +29,9 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
||||
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
||||
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
|
||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated.
|
||||
'DOCUMENT_SIGNING_CERTIFICATE_UPDATED', // When the include signing certificate is updated.
|
||||
'DOCUMENT_AUDIT_TRAIL_UPDATED', // When the include audit trail is updated.
|
||||
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
|
||||
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
||||
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
|
||||
@ -397,6 +399,16 @@ export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
||||
data: ZGenericFromToSchema,
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED),
|
||||
data: ZGenericFromToSchema,
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED),
|
||||
data: ZGenericFromToSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document global authentication access updated.
|
||||
*/
|
||||
@ -574,6 +586,8 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
||||
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
||||
ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
||||
|
||||
@ -18,6 +18,8 @@ import { ZRecipientLiteSchema } from './recipient';
|
||||
*/
|
||||
export const ZDocumentSchema = DocumentSchema.pick({
|
||||
visibility: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
status: true,
|
||||
source: true,
|
||||
id: true,
|
||||
@ -82,6 +84,8 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -104,6 +108,8 @@ export const ZDocumentManySchema = DocumentSchema.pick({
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
}).extend({
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
|
||||
@ -322,6 +322,14 @@ export const formatDocumentAuditLogAction = (
|
||||
anonymous: msg`Document visibility updated`,
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED }, () => ({
|
||||
anonymous: msg`Document signing certificate updated`,
|
||||
identified: msg`${prefix} updated the document signing certificate`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED }, () => ({
|
||||
anonymous: msg`Document audit trail updated`,
|
||||
identified: msg`${prefix} updated the document audit trail`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||
anonymous: msg`Document access auth updated`,
|
||||
identified: msg`${prefix} updated the document access auth requirements`,
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" ADD COLUMN "includeAuditTrail" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true;
|
||||
@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `includeAuditTrail` on the `Document` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" DROP COLUMN "includeAuditTrail",
|
||||
ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
||||
@ -311,6 +311,8 @@ model Document {
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
includeAuditTrailLog Boolean @default(false)
|
||||
title String
|
||||
status DocumentStatus @default(DRAFT)
|
||||
recipients Recipient[]
|
||||
|
||||
@ -63,6 +63,16 @@ export const ZDocumentVisibilitySchema = z
|
||||
.nativeEnum(DocumentVisibility)
|
||||
.describe('The visibility of the document.');
|
||||
|
||||
export const ZDocumentIncludeSigningCertificateSchema = z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to include a signing certificate in the document.');
|
||||
|
||||
export const ZDocumentIncludeAuditTrailSchema = z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to include an audit trail in the document.');
|
||||
|
||||
export const ZDocumentMetaTimezoneSchema = z
|
||||
.string()
|
||||
.describe(
|
||||
@ -237,6 +247,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
includeSigningCertificate: ZDocumentIncludeSigningCertificateSchema.optional(),
|
||||
includeAuditTrailLog: ZDocumentIncludeAuditTrailSchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
})
|
||||
|
||||
@ -42,6 +42,7 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Combobox } from '../combobox';
|
||||
import { Input } from '../input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
||||
@ -92,6 +93,8 @@ export const AddSettingsFormPartial = ({
|
||||
visibility: document.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
includeSigningCertificate: document.includeSigningCertificate ?? true,
|
||||
includeAuditTrailLog: document.includeAuditTrailLog ?? true,
|
||||
meta: {
|
||||
timezone:
|
||||
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
||||
@ -259,6 +262,111 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="globalActionAuth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Recipient action authentication</Trans>
|
||||
<DocumentGlobalAuthActionTooltip />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Accordion type="multiple" className="mt-6">
|
||||
<AccordionItem value="advanced-options" className="border-none">
|
||||
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
||||
<Trans>Certificates</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeSigningCertificate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
className="h-5 w-5"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="m-0 flex flex-row items-center">
|
||||
<Trans>Include signing certificate</Trans>{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<Trans>
|
||||
Including the signing certificate means that the certificate
|
||||
will be attached to the document. You won't be able to remove
|
||||
it. <br />
|
||||
<br />
|
||||
If you don't include it, you can download it individually.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeAuditTrailLog"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
className="h-5 w-5"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="m-0 flex flex-row items-center">
|
||||
<Trans>Include audit trail</Trans>{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<Trans>
|
||||
Including the audit trail means that the log of all actions will
|
||||
be attached to the document. You won't be able to remove it.{' '}
|
||||
<br />
|
||||
<br />
|
||||
If you don't include it, you can download it individually.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{isDocumentEnterprise && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@ -29,6 +29,8 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
includeSigningCertificate: z.boolean().default(true).optional(),
|
||||
includeAuditTrailLog: z.boolean().default(true).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
|
||||
83
packages/ui/primitives/split-button.tsx
Normal file
83
packages/ui/primitives/split-button.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { Button } from './button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from './dropdown-menu';
|
||||
|
||||
const SplitButtonContext = React.createContext<{
|
||||
variant?: React.ComponentProps<typeof Button>['variant'];
|
||||
size?: React.ComponentProps<typeof Button>['size'];
|
||||
}>({});
|
||||
|
||||
const SplitButton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
variant?: React.ComponentProps<typeof Button>['variant'];
|
||||
size?: React.ComponentProps<typeof Button>['size'];
|
||||
}
|
||||
>(({ className, children, variant = 'default', size = 'default', ...props }, ref) => {
|
||||
return (
|
||||
<SplitButtonContext.Provider value={{ variant, size }}>
|
||||
<div ref={ref} className={cn('inline-flex', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</SplitButtonContext.Provider>
|
||||
);
|
||||
});
|
||||
SplitButton.displayName = 'SplitButton';
|
||||
|
||||
const SplitButtonAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { variant, size } = React.useContext(SplitButtonContext);
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn('rounded-r-none border-r-0', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
SplitButtonAction.displayName = 'SplitButtonAction';
|
||||
|
||||
const SplitButtonDropdown = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { variant, size } = React.useContext(SplitButtonContext);
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className="rounded-l-none px-2 focus-visible:ring-offset-0"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<span className="sr-only">More options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" {...props} ref={ref}>
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
);
|
||||
SplitButtonDropdown.displayName = 'SplitButtonDropdown';
|
||||
|
||||
const SplitButtonDropdownItem = DropdownMenuItem;
|
||||
|
||||
export { SplitButton, SplitButtonAction, SplitButtonDropdown, SplitButtonDropdownItem };
|
||||
Reference in New Issue
Block a user