Add export document pdf options
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
164
apps/console/src/components/documents/BulkExportDialog.tsx
Normal file
164
apps/console/src/components/documents/BulkExportDialog.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
Field,
|
||||||
|
useDialogRef,
|
||||||
|
Spinner,
|
||||||
|
Checkbox,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { type ReactNode, useImperativeHandle, forwardRef } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
|
import { sprintf } from "@probo/helpers";
|
||||||
|
|
||||||
|
const bulkExportSchema = z.object({
|
||||||
|
withWatermark: z.boolean(),
|
||||||
|
watermarkEmail: z.string().optional().or(z.literal("")),
|
||||||
|
withSignatures: z.boolean(),
|
||||||
|
}).refine((data) => {
|
||||||
|
if (data.withWatermark && (!data.watermarkEmail || data.watermarkEmail === "")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.withWatermark && data.watermarkEmail && !z.string().email().safeParse(data.watermarkEmail).success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
message: "Please enter a valid email address",
|
||||||
|
path: ["watermarkEmail"],
|
||||||
|
});
|
||||||
|
|
||||||
|
type BulkExportFormData = z.infer<typeof bulkExportSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
onExport: (options: BulkExportFormData) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
defaultEmail?: string;
|
||||||
|
selectedCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BulkExportDialogRef = {
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
|
||||||
|
({ children, onExport, isLoading = false, defaultEmail = "", selectedCount }, ref) => {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema(
|
||||||
|
bulkExportSchema,
|
||||||
|
{
|
||||||
|
defaultValues: {
|
||||||
|
withWatermark: false,
|
||||||
|
watermarkEmail: defaultEmail,
|
||||||
|
withSignatures: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const watchWatermark = watch("withWatermark");
|
||||||
|
const watchSignatures = watch("withSignatures");
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: () => dialogRef.current?.open(),
|
||||||
|
close: () => dialogRef.current?.close(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
const options = {
|
||||||
|
...data,
|
||||||
|
watermarkEmail: data.withWatermark ? data.watermarkEmail : undefined,
|
||||||
|
};
|
||||||
|
onExport(options);
|
||||||
|
dialogRef.current?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClick={() => dialogRef.current?.open()}>{children}</div>
|
||||||
|
<Dialog
|
||||||
|
className="max-w-md"
|
||||||
|
ref={dialogRef}
|
||||||
|
title={sprintf(__("Export %s Documents"), selectedCount)}
|
||||||
|
>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<DialogContent className="space-y-4" padded>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={watchSignatures}
|
||||||
|
onChange={(checked) => setValue("withSignatures", checked)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
|
{__("Include signatures")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
|
{__("Show signature information and approval details in the PDFs")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={watchWatermark}
|
||||||
|
onChange={(checked) => setValue("withWatermark", checked)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
|
{__("Add watermark")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
|
{__("Add confidential watermark with email and timestamp to all PDFs")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{watchWatermark && (
|
||||||
|
<div className="ml-6">
|
||||||
|
<Field
|
||||||
|
label={__("Watermark email")}
|
||||||
|
{...register("watermarkEmail")}
|
||||||
|
type="email"
|
||||||
|
placeholder={__("Enter email address")}
|
||||||
|
error={formState.errors.watermarkEmail?.message}
|
||||||
|
autoComplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-level-1 p-3 rounded-lg border border-border-subtle">
|
||||||
|
<p className="text-sm text-txt-secondary">
|
||||||
|
{__("The documents will be exported as individual PDFs in a ZIP file. You will receive an email when the export is ready for download.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Spinner size={16} />
|
||||||
|
{__("Exporting...")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
__("Export Documents")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
141
apps/console/src/components/documents/PdfDownloadDialog.tsx
Normal file
141
apps/console/src/components/documents/PdfDownloadDialog.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
Field,
|
||||||
|
useDialogRef,
|
||||||
|
Spinner,
|
||||||
|
Checkbox,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { type ReactNode, useImperativeHandle, forwardRef } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
|
|
||||||
|
const pdfDownloadSchema = z.object({
|
||||||
|
withWatermark: z.boolean(),
|
||||||
|
watermarkEmail: z.string().email("Please enter a valid email address").optional().or(z.literal("")),
|
||||||
|
withSignatures: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PdfDownloadFormData = z.infer<typeof pdfDownloadSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
onDownload: (options: PdfDownloadFormData) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
defaultEmail?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PdfDownloadDialogRef = {
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
|
||||||
|
({ children, onDownload, isLoading = false, defaultEmail = "" }, ref) => {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema(
|
||||||
|
pdfDownloadSchema,
|
||||||
|
{
|
||||||
|
defaultValues: {
|
||||||
|
withWatermark: false,
|
||||||
|
watermarkEmail: defaultEmail,
|
||||||
|
withSignatures: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const watchWatermark = watch("withWatermark");
|
||||||
|
const watchSignatures = watch("withSignatures");
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: () => dialogRef.current?.open(),
|
||||||
|
close: () => dialogRef.current?.close(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
const options = {
|
||||||
|
...data,
|
||||||
|
watermarkEmail: data.withWatermark ? data.watermarkEmail : undefined,
|
||||||
|
};
|
||||||
|
onDownload(options);
|
||||||
|
dialogRef.current?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClick={() => dialogRef.current?.open()}>{children}</div>
|
||||||
|
<Dialog className="max-w-md" ref={dialogRef} title={__("Download PDF Options")}>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<DialogContent className="space-y-4" padded>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={watchSignatures}
|
||||||
|
onChange={(checked) => setValue("withSignatures", checked)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
|
{__("Include signatures")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
|
{__("Show signature information and approval details in the PDF")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={watchWatermark}
|
||||||
|
onChange={(checked) => setValue("withWatermark", checked)}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm font-medium text-txt-primary cursor-pointer">
|
||||||
|
{__("Add watermark")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-txt-secondary mt-1">
|
||||||
|
{__("Add confidential watermark with email and timestamp")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{watchWatermark && (
|
||||||
|
<div className="ml-6">
|
||||||
|
<Field
|
||||||
|
label={__("Watermark email")}
|
||||||
|
{...register("watermarkEmail")}
|
||||||
|
type="email"
|
||||||
|
placeholder={__("Enter email address")}
|
||||||
|
error={formState.errors.watermarkEmail?.message}
|
||||||
|
autoComplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Spinner size={16} />
|
||||||
|
{__("Downloading...")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
__("Download PDF")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<a398c636bba3d35aa7caef66c62c9ac6>>
|
* @generated SignedSource<<9b84adc0253186c6f0a5f47837d83d7f>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type BulkExportDocumentsInput = {
|
export type BulkExportDocumentsInput = {
|
||||||
documentIds: ReadonlyArray<string>;
|
documentIds: ReadonlyArray<string>;
|
||||||
|
watermarkEmail?: string | null | undefined;
|
||||||
|
withSignatures: boolean;
|
||||||
|
withWatermark: boolean;
|
||||||
};
|
};
|
||||||
export type DocumentGraphBulkExportDocumentsMutation$variables = {
|
export type DocumentGraphBulkExportDocumentsMutation$variables = {
|
||||||
input: BulkExportDocumentsInput;
|
input: BulkExportDocumentsInput;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
loadQuery,
|
loadQuery,
|
||||||
useFragment,
|
useFragment,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
|
useLazyLoadQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import type { DocumentGraphNodeQuery } from "/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql";
|
import type { DocumentGraphNodeQuery } from "/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ import type {
|
|||||||
} from "./__generated__/DocumentDetailPageDocumentFragment.graphql";
|
} from "./__generated__/DocumentDetailPageDocumentFragment.graphql";
|
||||||
import type { DocumentDetailPageExportPDFMutation } from "./__generated__/DocumentDetailPageExportPDFMutation.graphql";
|
import type { DocumentDetailPageExportPDFMutation } from "./__generated__/DocumentDetailPageExportPDFMutation.graphql";
|
||||||
import type { DocumentDetailPageUpdateMutation } from "./__generated__/DocumentDetailPageUpdateMutation.graphql";
|
import type { DocumentDetailPageUpdateMutation } from "./__generated__/DocumentDetailPageUpdateMutation.graphql";
|
||||||
|
import type { DocumentDetailPageUserEmailQuery } from "./__generated__/DocumentDetailPageUserEmailQuery.graphql";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
@@ -54,6 +56,7 @@ import {
|
|||||||
useParams,
|
useParams,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
import UpdateVersionDialog from "./dialogs/UpdateVersionDialog";
|
import UpdateVersionDialog from "./dialogs/UpdateVersionDialog";
|
||||||
|
import { PdfDownloadDialog, type PdfDownloadDialogRef } from "/components/documents/PdfDownloadDialog";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import type { NodeOf } from "/types.ts";
|
import type { NodeOf } from "/types.ts";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -185,6 +188,16 @@ const documentUpdateSchema = z.object({
|
|||||||
documentType: z.enum(documentTypes),
|
documentType: z.enum(documentTypes),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const UserEmailQuery = graphql`
|
||||||
|
query DocumentDetailPageUserEmailQuery {
|
||||||
|
viewer {
|
||||||
|
user {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default function DocumentDetailPage(props: Props) {
|
export default function DocumentDetailPage(props: Props) {
|
||||||
const { versionId } = useParams<{ versionId?: string }>();
|
const { versionId } = useParams<{ versionId?: string }>();
|
||||||
const node = usePreloadedQuery(documentNodeQuery, props.queryRef).node;
|
const node = usePreloadedQuery(documentNodeQuery, props.queryRef).node;
|
||||||
@@ -223,6 +236,9 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
errorMessage: __("Failed to generate PDF. Please try again."),
|
errorMessage: __("Failed to generate PDF. Please try again."),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userEmailData = useLazyLoadQuery<DocumentDetailPageUserEmailQuery>(UserEmailQuery, {});
|
||||||
|
const defaultEmail = userEmailData.viewer.user.email;
|
||||||
const [updateDocument, isUpdatingDocument] = useMutationWithToasts<DocumentDetailPageUpdateMutation>(
|
const [updateDocument, isUpdatingDocument] = useMutationWithToasts<DocumentDetailPageUpdateMutation>(
|
||||||
updateDocumentMutation,
|
updateDocumentMutation,
|
||||||
{
|
{
|
||||||
@@ -360,11 +376,16 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadPdf = () => {
|
const handleDownloadPdf = (options: { withWatermark: boolean; withSignatures: boolean; watermarkEmail?: string }) => {
|
||||||
|
const input = {
|
||||||
|
documentVersionId: currentVersion.id,
|
||||||
|
withWatermark: options.withWatermark,
|
||||||
|
withSignatures: options.withSignatures,
|
||||||
|
...(options.withWatermark && options.watermarkEmail && { watermarkEmail: options.watermarkEmail }),
|
||||||
|
};
|
||||||
|
|
||||||
exportDocumentVersionPDF({
|
exportDocumentVersionPDF({
|
||||||
variables: {
|
variables: { input },
|
||||||
input: { documentVersionId: currentVersion.id },
|
|
||||||
},
|
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
if (data.exportDocumentVersionPDF?.data) {
|
if (data.exportDocumentVersionPDF?.data) {
|
||||||
const link = window.document.createElement("a");
|
const link = window.document.createElement("a");
|
||||||
@@ -379,6 +400,7 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateDialogRef = useRef<{ open: () => void }>(null);
|
const updateDialogRef = useRef<{ open: () => void }>(null);
|
||||||
|
const pdfDownloadDialogRef = useRef<PdfDownloadDialogRef>(null);
|
||||||
const controlsCount = document.controlsInfo.totalCount;
|
const controlsCount = document.controlsInfo.totalCount;
|
||||||
const urlPrefix = versionId
|
const urlPrefix = versionId
|
||||||
? `/organizations/${organizationId}/documents/${document.id}/versions/${versionId}`
|
? `/organizations/${organizationId}/documents/${document.id}/versions/${versionId}`
|
||||||
@@ -391,6 +413,14 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
document={document}
|
document={document}
|
||||||
connectionId={versionConnectionId}
|
connectionId={versionConnectionId}
|
||||||
/>
|
/>
|
||||||
|
<PdfDownloadDialog
|
||||||
|
ref={pdfDownloadDialogRef}
|
||||||
|
onDownload={handleDownloadPdf}
|
||||||
|
isLoading={isExporting}
|
||||||
|
defaultEmail={defaultEmail}
|
||||||
|
>
|
||||||
|
{null}
|
||||||
|
</PdfDownloadDialog>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
@@ -451,7 +481,7 @@ export default function DocumentDetailPage(props: Props) {
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
)}
|
)}
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={handleDownloadPdf}
|
onClick={() => pdfDownloadDialogRef.current?.open()}
|
||||||
icon={IconArrowDown}
|
icon={IconArrowDown}
|
||||||
disabled={isExporting}
|
disabled={isExporting}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import {
|
|||||||
useFragment,
|
useFragment,
|
||||||
usePaginationFragment,
|
usePaginationFragment,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
|
useLazyLoadQuery,
|
||||||
type PreloadedQuery,
|
type PreloadedQuery,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
|
import { useRef } from "react";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/DocumentGraphListQuery.graphql";
|
import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/DocumentGraphListQuery.graphql";
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +47,8 @@ import type { DocumentsPageRowFragment$key } from "./__generated__/DocumentsPage
|
|||||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||||
import { PublishDocumentsDialog } from "./dialogs/PublishDocumentsDialog.tsx";
|
import { PublishDocumentsDialog } from "./dialogs/PublishDocumentsDialog.tsx";
|
||||||
import { SignatureDocumentsDialog } from "./dialogs/SignatureDocumentsDialog.tsx";
|
import { SignatureDocumentsDialog } from "./dialogs/SignatureDocumentsDialog.tsx";
|
||||||
|
import { BulkExportDialog, type BulkExportDialogRef } from "/components/documents/BulkExportDialog";
|
||||||
|
import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql";
|
||||||
|
|
||||||
const documentsFragment = graphql`
|
const documentsFragment = graphql`
|
||||||
fragment DocumentsPageListFragment on Organization
|
fragment DocumentsPageListFragment on Organization
|
||||||
@@ -81,6 +85,16 @@ type Props = {
|
|||||||
queryRef: PreloadedQuery<DocumentGraphListQuery>;
|
queryRef: PreloadedQuery<DocumentGraphListQuery>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UserEmailQuery = graphql`
|
||||||
|
query DocumentsPageUserEmailQuery {
|
||||||
|
viewer {
|
||||||
|
user {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default function DocumentsPage(props: Props) {
|
export default function DocumentsPage(props: Props) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
@@ -88,6 +102,9 @@ export default function DocumentsPage(props: Props) {
|
|||||||
documentsQuery,
|
documentsQuery,
|
||||||
props.queryRef
|
props.queryRef
|
||||||
).organization;
|
).organization;
|
||||||
|
|
||||||
|
const userEmailData = useLazyLoadQuery<DocumentsPageUserEmailQuery>(UserEmailQuery, {});
|
||||||
|
const defaultEmail = userEmailData.viewer.user.email;
|
||||||
const pagination = usePaginationFragment(
|
const pagination = usePaginationFragment(
|
||||||
documentsFragment,
|
documentsFragment,
|
||||||
organization as DocumentsPageListFragment$key
|
organization as DocumentsPageListFragment$key
|
||||||
@@ -99,9 +116,10 @@ export default function DocumentsPage(props: Props) {
|
|||||||
const connectionId = pagination.data.documents.__id;
|
const connectionId = pagination.data.documents.__id;
|
||||||
const [sendSigningNotifications] = useSendSigningNotificationsMutation();
|
const [sendSigningNotifications] = useSendSigningNotificationsMutation();
|
||||||
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
|
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
|
||||||
const [bulkExportDocuments] = useBulkExportDocumentsMutation();
|
const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation();
|
||||||
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
const { list: selection, toggle, clear, reset } = useList<string>([]);
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
const bulkExportDialogRef = useRef<BulkExportDialogRef>(null);
|
||||||
|
|
||||||
usePageTitle(__("Documents"));
|
usePageTitle(__("Documents"));
|
||||||
|
|
||||||
@@ -136,11 +154,16 @@ export default function DocumentsPage(props: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBulkExport = () => {
|
const handleBulkExport = (options: { withWatermark: boolean; withSignatures: boolean; watermarkEmail?: string }) => {
|
||||||
|
const input = {
|
||||||
|
documentIds: selection,
|
||||||
|
withWatermark: options.withWatermark,
|
||||||
|
withSignatures: options.withSignatures,
|
||||||
|
...(options.withWatermark && options.watermarkEmail && { watermarkEmail: options.watermarkEmail }),
|
||||||
|
};
|
||||||
|
|
||||||
bulkExportDocuments({
|
bulkExportDocuments({
|
||||||
variables: {
|
variables: { input },
|
||||||
input: { documentIds: selection },
|
|
||||||
},
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
clear();
|
clear();
|
||||||
});
|
});
|
||||||
@@ -216,14 +239,21 @@ export default function DocumentsPage(props: Props) {
|
|||||||
{__("Request signature")}
|
{__("Request signature")}
|
||||||
</Button>
|
</Button>
|
||||||
</SignatureDocumentsDialog>
|
</SignatureDocumentsDialog>
|
||||||
|
<BulkExportDialog
|
||||||
|
ref={bulkExportDialogRef}
|
||||||
|
onExport={handleBulkExport}
|
||||||
|
isLoading={isBulkExporting}
|
||||||
|
defaultEmail={defaultEmail}
|
||||||
|
selectedCount={selection.length}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={IconArrowDown}
|
icon={IconArrowDown}
|
||||||
onClick={handleBulkExport}
|
|
||||||
className="py-0.5 px-2 text-xs h-6 min-h-6"
|
className="py-0.5 px-2 text-xs h-6 min-h-6"
|
||||||
>
|
>
|
||||||
{__("Export")}
|
{__("Export")}
|
||||||
</Button>
|
</Button>
|
||||||
|
</BulkExportDialog>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
icon={IconTrashCan}
|
icon={IconTrashCan}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<804ed692b3f8c67b2a73ca09260e5d25>>
|
* @generated SignedSource<<f860e097afe4111c629ecf78d873a84e>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
import { ConcreteRequest } from 'relay-runtime';
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
export type ExportDocumentVersionPDFInput = {
|
export type ExportDocumentVersionPDFInput = {
|
||||||
documentVersionId: string;
|
documentVersionId: string;
|
||||||
|
watermarkEmail?: string | null | undefined;
|
||||||
|
withSignatures: boolean;
|
||||||
|
withWatermark: boolean;
|
||||||
};
|
};
|
||||||
export type DocumentDetailPageExportPDFMutation$variables = {
|
export type DocumentDetailPageExportPDFMutation$variables = {
|
||||||
input: ExportDocumentVersionPDFInput;
|
input: ExportDocumentVersionPDFInput;
|
||||||
|
|||||||
120
apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageUserEmailQuery.graphql.ts
generated
Normal file
120
apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageUserEmailQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<c1a6e41382db3951401d70c039c49f01>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DocumentDetailPageUserEmailQuery$variables = Record<PropertyKey, never>;
|
||||||
|
export type DocumentDetailPageUserEmailQuery$data = {
|
||||||
|
readonly viewer: {
|
||||||
|
readonly user: {
|
||||||
|
readonly email: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type DocumentDetailPageUserEmailQuery = {
|
||||||
|
response: DocumentDetailPageUserEmailQuery$data;
|
||||||
|
variables: DocumentDetailPageUserEmailQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "email",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "DocumentDetailPageUserEmailQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Viewer",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "viewer",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "user",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "DocumentDetailPageUserEmailQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Viewer",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "viewer",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "user",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "9321ba3f2fb159e06a07bfa9b2d91922",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "DocumentDetailPageUserEmailQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query DocumentDetailPageUserEmailQuery {\n viewer {\n user {\n email\n id\n }\n id\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "b4ffc243e94463feab8fd6555a674c58";
|
||||||
|
|
||||||
|
export default node;
|
||||||
120
apps/console/src/pages/organizations/documents/__generated__/DocumentsPageUserEmailQuery.graphql.ts
generated
Normal file
120
apps/console/src/pages/organizations/documents/__generated__/DocumentsPageUserEmailQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<7ff59bf602dca8eafe0a671cb7e95aad>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DocumentsPageUserEmailQuery$variables = Record<PropertyKey, never>;
|
||||||
|
export type DocumentsPageUserEmailQuery$data = {
|
||||||
|
readonly viewer: {
|
||||||
|
readonly user: {
|
||||||
|
readonly email: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type DocumentsPageUserEmailQuery = {
|
||||||
|
response: DocumentsPageUserEmailQuery$data;
|
||||||
|
variables: DocumentsPageUserEmailQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "email",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "DocumentsPageUserEmailQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Viewer",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "viewer",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "user",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "DocumentsPageUserEmailQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Viewer",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "viewer",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "user",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "d2d6cd91f55d59dc35d1c919f7760bac",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "DocumentsPageUserEmailQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query DocumentsPageUserEmailQuery {\n viewer {\n user {\n email\n id\n }\n id\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "e8af56c98fa637c3e7487374fb2eaa0e";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -32,6 +32,9 @@ type (
|
|||||||
|
|
||||||
DocumentExportArguments struct {
|
DocumentExportArguments struct {
|
||||||
DocumentIDs []gid.GID `json:"document_ids"`
|
DocumentIDs []gid.GID `json:"document_ids"`
|
||||||
|
WithWatermark bool `json:"with_watermark"`
|
||||||
|
WatermarkEmail *string `json:"watermark_email"`
|
||||||
|
WithSignatures bool `json:"with_signatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameworkExportArguments struct {
|
FrameworkExportArguments struct {
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ func TestRenderHTML(t *testing.T) {
|
|||||||
"<td>1</td>",
|
"<td>1</td>",
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"John Doe",
|
"John Doe",
|
||||||
"Test document description",
|
|
||||||
"Alice Smith",
|
"Alice Smith",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -80,7 +79,6 @@ func TestRenderHTML(t *testing.T) {
|
|||||||
wantContains: []string{
|
wantContains: []string{
|
||||||
"Test &amp; &lt;Script&gt; Title",
|
"Test &amp; &lt;Script&gt; Title",
|
||||||
"John &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; Doe",
|
"John &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt; Doe",
|
||||||
"Description with &amp; symbols and &lt;tags&gt;",
|
|
||||||
"Alice &amp; &lt;Bob&gt;",
|
"Alice &amp; &lt;Bob&gt;",
|
||||||
},
|
},
|
||||||
wantNotContains: []string{
|
wantNotContains: []string{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/mail"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
"github.com/getprobo/probo/pkg/html2pdf"
|
"github.com/getprobo/probo/pkg/html2pdf"
|
||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
"github.com/getprobo/probo/pkg/statelesstoken"
|
"github.com/getprobo/probo/pkg/statelesstoken"
|
||||||
|
"github.com/getprobo/probo/pkg/watermarkpdf"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/crypto/uuid"
|
"go.gearno.de/crypto/uuid"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
@@ -833,10 +835,20 @@ func (s *DocumentService) RequestExport(
|
|||||||
documentIDs []gid.GID,
|
documentIDs []gid.GID,
|
||||||
recipientEmail string,
|
recipientEmail string,
|
||||||
recipientName string,
|
recipientName string,
|
||||||
|
options BulkExportOptions,
|
||||||
) (*coredata.ExportJob, error) {
|
) (*coredata.ExportJob, error) {
|
||||||
var exportJobID gid.GID
|
var exportJobID gid.GID
|
||||||
exportJob := &coredata.ExportJob{}
|
exportJob := &coredata.ExportJob{}
|
||||||
|
|
||||||
|
if options.WithWatermark {
|
||||||
|
if options.WatermarkEmail == nil {
|
||||||
|
return nil, fmt.Errorf("watermark email is required when with watermark is true")
|
||||||
|
}
|
||||||
|
if _, err := mail.ParseAddress(*options.WatermarkEmail); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid email address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
|
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
|
||||||
for _, documentID := range documentIDs {
|
for _, documentID := range documentIDs {
|
||||||
document := &coredata.Document{}
|
document := &coredata.Document{}
|
||||||
@@ -850,6 +862,9 @@ func (s *DocumentService) RequestExport(
|
|||||||
|
|
||||||
args := coredata.DocumentExportArguments{
|
args := coredata.DocumentExportArguments{
|
||||||
DocumentIDs: documentIDs,
|
DocumentIDs: documentIDs,
|
||||||
|
WithWatermark: options.WithWatermark,
|
||||||
|
WatermarkEmail: options.WatermarkEmail,
|
||||||
|
WithSignatures: options.WithSignatures,
|
||||||
}
|
}
|
||||||
argsJSON, err := json.Marshal(args)
|
argsJSON, err := json.Marshal(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1159,16 +1174,29 @@ func (s *DocumentService) CancelSignatureRequest(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportPDFOptions struct {
|
||||||
|
WithWatermark bool
|
||||||
|
WatermarkEmail *string
|
||||||
|
WithSignatures bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkExportOptions struct {
|
||||||
|
WithWatermark bool
|
||||||
|
WatermarkEmail *string
|
||||||
|
WithSignatures bool
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DocumentService) ExportPDF(
|
func (s *DocumentService) ExportPDF(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
documentVersionID gid.GID,
|
documentVersionID gid.GID,
|
||||||
|
options ExportPDFOptions,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
var data []byte
|
var data []byte
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) (err error) {
|
func(conn pg.Conn) (err error) {
|
||||||
data, err = exportDocumentPDF(ctx, s.html2pdfConverter, conn, s.svc.scope, documentVersionID)
|
data, err = exportDocumentPDF(ctx, s.html2pdfConverter, conn, s.svc.scope, documentVersionID, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot export document PDF: %w", err)
|
return fmt.Errorf("cannot export document PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1206,7 +1234,18 @@ func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID
|
|||||||
defer tempFile.Close()
|
defer tempFile.Close()
|
||||||
defer os.Remove(tempFile.Name())
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
err = s.Export(ctx, documentIDs, tempFile)
|
exportArgs, err := exportJob.GetDocumentExportArguments()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get export arguments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exportOptions := BulkExportOptions{
|
||||||
|
WithWatermark: exportArgs.WithWatermark,
|
||||||
|
WatermarkEmail: exportArgs.WatermarkEmail,
|
||||||
|
WithSignatures: exportArgs.WithSignatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Export(ctx, documentIDs, tempFile, exportOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot export documents: %w", err)
|
return fmt.Errorf("cannot export documents: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1281,6 +1320,7 @@ func exportDocumentPDF(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope coredata.Scoper,
|
scope coredata.Scoper,
|
||||||
documentVersionID gid.GID,
|
documentVersionID gid.GID,
|
||||||
|
options ExportPDFOptions,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
document := &coredata.Document{}
|
document := &coredata.Document{}
|
||||||
version := &coredata.DocumentVersion{}
|
version := &coredata.DocumentVersion{}
|
||||||
@@ -1296,6 +1336,12 @@ func exportDocumentPDF(
|
|||||||
return nil, fmt.Errorf("cannot load document: %w", err)
|
return nil, fmt.Errorf("cannot load document: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := owner.LoadByID(ctx, conn, scope, document.OwnerID); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load document owner: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signatureData []docgen.SignatureData
|
||||||
|
if options.WithSignatures {
|
||||||
cursor := page.NewCursor(
|
cursor := page.NewCursor(
|
||||||
100,
|
100,
|
||||||
nil,
|
nil,
|
||||||
@@ -1310,10 +1356,6 @@ func exportDocumentPDF(
|
|||||||
return nil, fmt.Errorf("cannot load document version signatures: %w", err)
|
return nil, fmt.Errorf("cannot load document version signatures: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := owner.LoadByID(ctx, conn, scope, document.OwnerID); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot load document owner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: refactor this to use a single query
|
// TODO: refactor this to use a single query
|
||||||
for _, sig := range signatures {
|
for _, sig := range signatures {
|
||||||
if _, ok := peopleMap[sig.SignedBy]; !ok {
|
if _, ok := peopleMap[sig.SignedBy]; !ok {
|
||||||
@@ -1325,6 +1367,17 @@ func exportDocumentPDF(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signatureData = make([]docgen.SignatureData, len(signatures))
|
||||||
|
for i, sig := range signatures {
|
||||||
|
signatureData[i] = docgen.SignatureData{
|
||||||
|
SignedBy: peopleMap[sig.SignedBy].FullName,
|
||||||
|
SignedAt: sig.SignedAt,
|
||||||
|
State: sig.State,
|
||||||
|
RequestedAt: sig.RequestedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
classification := docgen.ClassificationInternal
|
classification := docgen.ClassificationInternal
|
||||||
switch document.DocumentType {
|
switch document.DocumentType {
|
||||||
case coredata.DocumentTypePolicy:
|
case coredata.DocumentTypePolicy:
|
||||||
@@ -1340,16 +1393,7 @@ func exportDocumentPDF(
|
|||||||
Classification: classification,
|
Classification: classification,
|
||||||
Approver: owner.FullName,
|
Approver: owner.FullName,
|
||||||
PublishedAt: version.PublishedAt,
|
PublishedAt: version.PublishedAt,
|
||||||
Signatures: make([]docgen.SignatureData, len(signatures)),
|
Signatures: signatureData,
|
||||||
}
|
|
||||||
|
|
||||||
for i, sig := range signatures {
|
|
||||||
docData.Signatures[i] = docgen.SignatureData{
|
|
||||||
SignedBy: peopleMap[sig.SignedBy].FullName,
|
|
||||||
SignedAt: sig.SignedAt,
|
|
||||||
State: sig.State,
|
|
||||||
RequestedAt: sig.RequestedAt,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlContent, err := docgen.RenderHTML(docData)
|
htmlContent, err := docgen.RenderHTML(docData)
|
||||||
@@ -1377,6 +1421,22 @@ func exportDocumentPDF(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot read PDF data: %w", err)
|
return nil, fmt.Errorf("cannot read PDF data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.WithWatermark {
|
||||||
|
if options.WatermarkEmail == nil {
|
||||||
|
return nil, fmt.Errorf("watermark email is required with watermark enabled")
|
||||||
|
}
|
||||||
|
if _, err := mail.ParseAddress(*options.WatermarkEmail); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid email address")
|
||||||
|
}
|
||||||
|
|
||||||
|
watermarkedPDF, err := watermarkpdf.AddConfidentialWithTimestamp(pdfData, *options.WatermarkEmail)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot add watermark to PDF: %w", err)
|
||||||
|
}
|
||||||
|
return watermarkedPDF, nil
|
||||||
|
}
|
||||||
|
|
||||||
return pdfData, nil
|
return pdfData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1384,6 +1444,7 @@ func (s *DocumentService) Export(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
documentIDs []gid.GID,
|
documentIDs []gid.GID,
|
||||||
file io.Writer,
|
file io.Writer,
|
||||||
|
options BulkExportOptions,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
archive := zip.NewWriter(file)
|
archive := zip.NewWriter(file)
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -1406,12 +1467,19 @@ func (s *DocumentService) Export(
|
|||||||
return fmt.Errorf("cannot load document version for %q: %w", documentID, err)
|
return fmt.Errorf("cannot load document version for %q: %w", documentID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pdfOptions := ExportPDFOptions{
|
||||||
|
WithWatermark: options.WithWatermark,
|
||||||
|
WatermarkEmail: options.WatermarkEmail,
|
||||||
|
WithSignatures: options.WithSignatures,
|
||||||
|
}
|
||||||
|
|
||||||
exportedPDF, err := exportDocumentPDF(
|
exportedPDF, err := exportDocumentPDF(
|
||||||
ctx,
|
ctx,
|
||||||
s.html2pdfConverter,
|
s.html2pdfConverter,
|
||||||
conn,
|
conn,
|
||||||
s.svc.scope,
|
s.svc.scope,
|
||||||
documentVersion.ID,
|
documentVersion.ID,
|
||||||
|
pdfOptions,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot export document PDF for %q: %w", documentID, err)
|
return fmt.Errorf("cannot export document PDF for %q: %w", documentID, err)
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ func (s FrameworkService) Export(
|
|||||||
conn,
|
conn,
|
||||||
s.svc.scope,
|
s.svc.scope,
|
||||||
documentVersion.ID,
|
documentVersion.ID,
|
||||||
|
ExportPDFOptions{WithSignatures: true},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot export document PDF: %w", err)
|
return fmt.Errorf("cannot export document PDF: %w", err)
|
||||||
|
|||||||
@@ -3303,6 +3303,9 @@ input UpdateDocumentInput {
|
|||||||
|
|
||||||
input ExportDocumentVersionPDFInput {
|
input ExportDocumentVersionPDFInput {
|
||||||
documentVersionId: ID!
|
documentVersionId: ID!
|
||||||
|
withWatermark: Boolean!
|
||||||
|
watermarkEmail: String
|
||||||
|
withSignatures: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input DeleteDocumentInput {
|
input DeleteDocumentInput {
|
||||||
@@ -4002,6 +4005,9 @@ input BulkDeleteDocumentsInput {
|
|||||||
|
|
||||||
input BulkExportDocumentsInput {
|
input BulkExportDocumentsInput {
|
||||||
documentIds: [ID!]!
|
documentIds: [ID!]!
|
||||||
|
withWatermark: Boolean!
|
||||||
|
watermarkEmail: String
|
||||||
|
withSignatures: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkDeleteDocumentsPayload {
|
type BulkDeleteDocumentsPayload {
|
||||||
|
|||||||
@@ -11713,6 +11713,9 @@ input UpdateDocumentInput {
|
|||||||
|
|
||||||
input ExportDocumentVersionPDFInput {
|
input ExportDocumentVersionPDFInput {
|
||||||
documentVersionId: ID!
|
documentVersionId: ID!
|
||||||
|
withWatermark: Boolean!
|
||||||
|
watermarkEmail: String
|
||||||
|
withSignatures: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input DeleteDocumentInput {
|
input DeleteDocumentInput {
|
||||||
@@ -12412,6 +12415,9 @@ input BulkDeleteDocumentsInput {
|
|||||||
|
|
||||||
input BulkExportDocumentsInput {
|
input BulkExportDocumentsInput {
|
||||||
documentIds: [ID!]!
|
documentIds: [ID!]!
|
||||||
|
withWatermark: Boolean!
|
||||||
|
watermarkEmail: String
|
||||||
|
withSignatures: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkDeleteDocumentsPayload {
|
type BulkDeleteDocumentsPayload {
|
||||||
@@ -63447,7 +63453,7 @@ func (ec *executionContext) unmarshalInputBulkExportDocumentsInput(ctx context.C
|
|||||||
asMap[k] = v
|
asMap[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldsInOrder := [...]string{"documentIds"}
|
fieldsInOrder := [...]string{"documentIds", "withWatermark", "watermarkEmail", "withSignatures"}
|
||||||
for _, k := range fieldsInOrder {
|
for _, k := range fieldsInOrder {
|
||||||
v, ok := asMap[k]
|
v, ok := asMap[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -63461,6 +63467,27 @@ func (ec *executionContext) unmarshalInputBulkExportDocumentsInput(ctx context.C
|
|||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
it.DocumentIds = data
|
it.DocumentIds = data
|
||||||
|
case "withWatermark":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withWatermark"))
|
||||||
|
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.WithWatermark = data
|
||||||
|
case "watermarkEmail":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("watermarkEmail"))
|
||||||
|
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.WatermarkEmail = data
|
||||||
|
case "withSignatures":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withSignatures"))
|
||||||
|
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.WithSignatures = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66867,7 +66894,7 @@ func (ec *executionContext) unmarshalInputExportDocumentVersionPDFInput(ctx cont
|
|||||||
asMap[k] = v
|
asMap[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldsInOrder := [...]string{"documentVersionId"}
|
fieldsInOrder := [...]string{"documentVersionId", "withWatermark", "watermarkEmail", "withSignatures"}
|
||||||
for _, k := range fieldsInOrder {
|
for _, k := range fieldsInOrder {
|
||||||
v, ok := asMap[k]
|
v, ok := asMap[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -66881,6 +66908,27 @@ func (ec *executionContext) unmarshalInputExportDocumentVersionPDFInput(ctx cont
|
|||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
it.DocumentVersionID = data
|
it.DocumentVersionID = data
|
||||||
|
case "withWatermark":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withWatermark"))
|
||||||
|
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.WithWatermark = data
|
||||||
|
case "watermarkEmail":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("watermarkEmail"))
|
||||||
|
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.WatermarkEmail = data
|
||||||
|
case "withSignatures":
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("withSignatures"))
|
||||||
|
data, err := ec.unmarshalNBoolean2bool(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
it.WithSignatures = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ type BulkDeleteDocumentsPayload struct {
|
|||||||
|
|
||||||
type BulkExportDocumentsInput struct {
|
type BulkExportDocumentsInput struct {
|
||||||
DocumentIds []gid.GID `json:"documentIds"`
|
DocumentIds []gid.GID `json:"documentIds"`
|
||||||
|
WithWatermark bool `json:"withWatermark"`
|
||||||
|
WatermarkEmail *string `json:"watermarkEmail,omitempty"`
|
||||||
|
WithSignatures bool `json:"withSignatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkExportDocumentsPayload struct {
|
type BulkExportDocumentsPayload struct {
|
||||||
@@ -1053,6 +1056,9 @@ type EvidenceEdge struct {
|
|||||||
|
|
||||||
type ExportDocumentVersionPDFInput struct {
|
type ExportDocumentVersionPDFInput struct {
|
||||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||||
|
WithWatermark bool `json:"withWatermark"`
|
||||||
|
WatermarkEmail *string `json:"watermarkEmail,omitempty"`
|
||||||
|
WithSignatures bool `json:"withSignatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExportDocumentVersionPDFPayload struct {
|
type ExportDocumentVersionPDFPayload struct {
|
||||||
|
|||||||
@@ -2628,7 +2628,13 @@ func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.
|
|||||||
panic(fmt.Errorf("user not found"))
|
panic(fmt.Errorf("user not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, user.EmailAddress, user.FullName)
|
options := probo.BulkExportOptions{
|
||||||
|
WithWatermark: input.WithWatermark,
|
||||||
|
WithSignatures: input.WithSignatures,
|
||||||
|
WatermarkEmail: input.WatermarkEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, user.EmailAddress, user.FullName, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot request document export: %w", err))
|
panic(fmt.Errorf("cannot request document export: %w", err))
|
||||||
}
|
}
|
||||||
@@ -2775,7 +2781,13 @@ func (r *mutationResolver) CancelSignatureRequest(ctx context.Context, input typ
|
|||||||
func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error) {
|
func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error) {
|
||||||
prb := r.ProboService(ctx, input.DocumentVersionID.TenantID())
|
prb := r.ProboService(ctx, input.DocumentVersionID.TenantID())
|
||||||
|
|
||||||
pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID)
|
options := probo.ExportPDFOptions{
|
||||||
|
WithSignatures: input.WithSignatures,
|
||||||
|
WithWatermark: input.WithWatermark,
|
||||||
|
WatermarkEmail: input.WatermarkEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("cannot export document version PDF: %w", err))
|
panic(fmt.Errorf("cannot export document version PDF: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user