Add export document pdf options

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-30 10:49:34 +02:00
parent f61ad48b59
commit 56a666b7e8
16 changed files with 814 additions and 61 deletions

View File

@@ -4,6 +4,7 @@ import {
loadQuery,
useFragment,
usePreloadedQuery,
useLazyLoadQuery,
} from "react-relay";
import type { DocumentGraphNodeQuery } from "/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql";
import {
@@ -18,6 +19,7 @@ import type {
} from "./__generated__/DocumentDetailPageDocumentFragment.graphql";
import type { DocumentDetailPageExportPDFMutation } from "./__generated__/DocumentDetailPageExportPDFMutation.graphql";
import type { DocumentDetailPageUpdateMutation } from "./__generated__/DocumentDetailPageUpdateMutation.graphql";
import type { DocumentDetailPageUserEmailQuery } from "./__generated__/DocumentDetailPageUserEmailQuery.graphql";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
@@ -54,6 +56,7 @@ import {
useParams,
} from "react-router";
import UpdateVersionDialog from "./dialogs/UpdateVersionDialog";
import { PdfDownloadDialog, type PdfDownloadDialogRef } from "/components/documents/PdfDownloadDialog";
import { useRef, useState } from "react";
import type { NodeOf } from "/types.ts";
import clsx from "clsx";
@@ -185,6 +188,16 @@ const documentUpdateSchema = z.object({
documentType: z.enum(documentTypes),
});
const UserEmailQuery = graphql`
query DocumentDetailPageUserEmailQuery {
viewer {
user {
email
}
}
}
`;
export default function DocumentDetailPage(props: Props) {
const { versionId } = useParams<{ versionId?: string }>();
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."),
}
);
const userEmailData = useLazyLoadQuery<DocumentDetailPageUserEmailQuery>(UserEmailQuery, {});
const defaultEmail = userEmailData.viewer.user.email;
const [updateDocument, isUpdatingDocument] = useMutationWithToasts<DocumentDetailPageUpdateMutation>(
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({
variables: {
input: { documentVersionId: currentVersion.id },
},
variables: { input },
onCompleted: (data) => {
if (data.exportDocumentVersionPDF?.data) {
const link = window.document.createElement("a");
@@ -379,6 +400,7 @@ export default function DocumentDetailPage(props: Props) {
};
const updateDialogRef = useRef<{ open: () => void }>(null);
const pdfDownloadDialogRef = useRef<PdfDownloadDialogRef>(null);
const controlsCount = document.controlsInfo.totalCount;
const urlPrefix = versionId
? `/organizations/${organizationId}/documents/${document.id}/versions/${versionId}`
@@ -391,6 +413,14 @@ export default function DocumentDetailPage(props: Props) {
document={document}
connectionId={versionConnectionId}
/>
<PdfDownloadDialog
ref={pdfDownloadDialogRef}
onDownload={handleDownloadPdf}
isLoading={isExporting}
defaultEmail={defaultEmail}
>
{null}
</PdfDownloadDialog>
<div className="space-y-6">
<div className="flex justify-between items-center mb-4">
<Breadcrumb
@@ -451,7 +481,7 @@ export default function DocumentDetailPage(props: Props) {
</DropdownItem>
)}
<DropdownItem
onClick={handleDownloadPdf}
onClick={() => pdfDownloadDialogRef.current?.open()}
icon={IconArrowDown}
disabled={isExporting}
>

View File

@@ -26,8 +26,10 @@ import {
useFragment,
usePaginationFragment,
usePreloadedQuery,
useLazyLoadQuery,
type PreloadedQuery,
} from "react-relay";
import { useRef } from "react";
import { graphql } from "relay-runtime";
import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/DocumentGraphListQuery.graphql";
import {
@@ -45,6 +47,8 @@ import type { DocumentsPageRowFragment$key } from "./__generated__/DocumentsPage
import { SortableTable, SortableTh } from "/components/SortableTable";
import { PublishDocumentsDialog } from "./dialogs/PublishDocumentsDialog.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`
fragment DocumentsPageListFragment on Organization
@@ -81,6 +85,16 @@ type Props = {
queryRef: PreloadedQuery<DocumentGraphListQuery>;
};
const UserEmailQuery = graphql`
query DocumentsPageUserEmailQuery {
viewer {
user {
email
}
}
}
`;
export default function DocumentsPage(props: Props) {
const { __ } = useTranslate();
@@ -88,6 +102,9 @@ export default function DocumentsPage(props: Props) {
documentsQuery,
props.queryRef
).organization;
const userEmailData = useLazyLoadQuery<DocumentsPageUserEmailQuery>(UserEmailQuery, {});
const defaultEmail = userEmailData.viewer.user.email;
const pagination = usePaginationFragment(
documentsFragment,
organization as DocumentsPageListFragment$key
@@ -99,9 +116,10 @@ export default function DocumentsPage(props: Props) {
const connectionId = pagination.data.documents.__id;
const [sendSigningNotifications] = useSendSigningNotificationsMutation();
const [bulkDeleteDocuments] = useBulkDeleteDocumentsMutation();
const [bulkExportDocuments] = useBulkExportDocumentsMutation();
const [bulkExportDocuments, isBulkExporting] = useBulkExportDocumentsMutation();
const { list: selection, toggle, clear, reset } = useList<string>([]);
const confirm = useConfirm();
const bulkExportDialogRef = useRef<BulkExportDialogRef>(null);
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({
variables: {
input: { documentIds: selection },
},
variables: { input },
}).then(() => {
clear();
});
@@ -216,14 +239,21 @@ export default function DocumentsPage(props: Props) {
{__("Request signature")}
</Button>
</SignatureDocumentsDialog>
<Button
variant="secondary"
icon={IconArrowDown}
onClick={handleBulkExport}
className="py-0.5 px-2 text-xs h-6 min-h-6"
<BulkExportDialog
ref={bulkExportDialogRef}
onExport={handleBulkExport}
isLoading={isBulkExporting}
defaultEmail={defaultEmail}
selectedCount={selection.length}
>
{__("Export")}
</Button>
<Button
variant="secondary"
icon={IconArrowDown}
className="py-0.5 px-2 text-xs h-6 min-h-6"
>
{__("Export")}
</Button>
</BulkExportDialog>
<Button
variant="danger"
icon={IconTrashCan}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<804ed692b3f8c67b2a73ca09260e5d25>>
* @generated SignedSource<<f860e097afe4111c629ecf78d873a84e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,6 +11,9 @@
import { ConcreteRequest } from 'relay-runtime';
export type ExportDocumentVersionPDFInput = {
documentVersionId: string;
watermarkEmail?: string | null | undefined;
withSignatures: boolean;
withWatermark: boolean;
};
export type DocumentDetailPageExportPDFMutation$variables = {
input: ExportDocumentVersionPDFInput;

View 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;

View 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;