Add request signature dialog

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Grafikart
2025-07-11 18:04:37 +02:00
committed by Sacha Al Himdani
parent 8fd8319ad4
commit 9f03bc1873
4 changed files with 193 additions and 53 deletions

View File

@@ -56,7 +56,7 @@ export const paginatedPeopleQuery = graphql`
}
`;
const paginatedPeopleFragment = graphql`
export const paginatedPeopleFragment = graphql`
fragment PeopleGraphPaginatedFragment on Organization
@refetchable(queryName: "PeopleListQuery")
@argumentDefinitions(

View File

@@ -39,9 +39,9 @@ import { sprintf, getDocumentTypeLabel } from "@probo/helpers";
import { CreateDocumentDialog } from "./dialogs/CreateDocumentDialog";
import type { DocumentsPageRowFragment$key } from "./__generated__/DocumentsPageRowFragment.graphql";
import { SortableTable, SortableTh } from "/components/SortableTable";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts.ts";
import { usePeople } from "/hooks/graph/PeopleGraph.ts";
import { PublishDocumentsDialog } from "./dialogs/PublishDocumentsDialog.tsx";
import { SignatureDocumentsDialog } from "./dialogs/SignatureDocumentsDialog.tsx";
const documentsFragment = graphql`
fragment DocumentsPageListFragment on Organization
@@ -74,19 +74,6 @@ const documentsFragment = graphql`
}
`;
const documentsSignatureMutation = graphql`
mutation DocumentsPageSignatureMutation($input: BulkRequestSignaturesInput!) {
bulkRequestSignatures(input: $input) {
documentVersionSignatureEdges {
node {
id
state
}
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<DocumentGraphListQuery>;
};
@@ -108,13 +95,6 @@ export default function DocumentsPage(props: Props) {
const [sendSigningNotifications] = useSendSigningNotificationsMutation();
const { list: selection, toggle, clear, reset } = useList<string>([]);
const [signatureMutation, isSigning] = useMutationWithToasts(
documentsSignatureMutation,
{
successMessage: __("Signature request send successfully"),
errorMessage: __("Failed to send signature requests"),
}
);
const people = usePeople(organization.id);
usePageTitle(__("Documents"));
@@ -126,18 +106,6 @@ export default function DocumentsPage(props: Props) {
});
};
const requestSignatures = async () => {
await signatureMutation({
variables: {
input: {
documentIds: selection,
signatoryIds: people.map((p) => p.id),
},
},
});
clear();
};
return (
<div className="space-y-6">
<PageHeader
@@ -197,14 +165,14 @@ export default function DocumentsPage(props: Props) {
>
<Button icon={IconCheckmark1}>{__("Publish")}</Button>
</PublishDocumentsDialog>
<Button
variant="secondary"
icon={IconSignature}
onClick={requestSignatures}
disabled={isSigning}
<SignatureDocumentsDialog
documentIds={selection}
onSave={clear}
>
{__("Request signature")}
</Button>
<Button variant="secondary" icon={IconSignature}>
{__("Request signature")}
</Button>
</SignatureDocumentsDialog>
</div>
</div>
</Th>

View File

@@ -0,0 +1,172 @@
import {
Button,
Dialog,
DialogContent,
DialogFooter,
useDialogRef,
Breadcrumb,
Tr,
Tbody,
Td,
Avatar,
Spinner,
Checkbox,
Table,
IconChevronDown,
} from "@probo/ui";
import { Suspense, type FormEventHandler, type ReactNode } from "react";
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import { sprintf } from "@probo/helpers";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts.ts";
import { useOrganizationId } from "/hooks/useOrganizationId.ts";
import {
paginatedPeopleFragment,
paginatedPeopleQuery,
} from "/hooks/graph/PeopleGraph.ts";
import { useList } from "@probo/hooks";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import type { PeopleGraphPaginatedFragment$key } from "/hooks/graph/__generated__/PeopleGraphPaginatedFragment.graphql.ts";
import type { PeopleGraphPaginatedQuery } from "/hooks/graph/__generated__/PeopleGraphPaginatedQuery.graphql.ts";
type Props = {
documentIds: string[];
children: ReactNode;
onSave: () => void;
};
const documentsSignatureMutation = graphql`
mutation SignatureDocumentsDialogMutation(
$input: BulkRequestSignaturesInput!
) {
bulkRequestSignatures(input: $input) {
documentVersionSignatureEdges {
node {
id
state
}
}
}
}
`;
export function SignatureDocumentsDialog({
documentIds,
children,
onSave,
}: Props) {
const { __ } = useTranslate();
const dialogRef = useDialogRef();
const { list: selectedPeople, toggle } = useList<string>([]);
const [publishMutation] = useMutationWithToasts(documentsSignatureMutation, {
successMessage: sprintf(
__("%s signature requests sent"),
documentIds.length
),
errorMessage: __("Failed to send signature requests"),
});
const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
await publishMutation({
variables: {
input: {
documentIds,
signatoryIds: selectedPeople,
},
},
});
dialogRef.current?.close();
onSave();
};
return (
<Dialog
className="max-w-xl"
ref={dialogRef}
trigger={children}
title={<Breadcrumb items={[__("Documents"), __("Signature requests")]} />}
>
<form onSubmit={onSubmit}>
<DialogContent>
<Suspense fallback={<Spinner />}>
<PeopleList onChange={toggle} selectedPeople={selectedPeople} />
</Suspense>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={selectedPeople.length === 0}>
{__("Send signature requests")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}
function PeopleList({
onChange,
selectedPeople,
}: {
onChange: (id: string) => void;
selectedPeople: string[];
}) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const data = useLazyLoadQuery<PeopleGraphPaginatedQuery>(
paginatedPeopleQuery,
{
organizationId,
}
);
const {
data: page,
hasNext,
loadNext,
isLoadingNext,
} = usePaginationFragment(
paginatedPeopleFragment,
data.organization as PeopleGraphPaginatedFragment$key
);
const people = page.peoples.edges.map((edge) => edge.node);
return (
<>
<Table className="border-none rounded-none">
<Tbody>
{people.map((person) => (
<Tr key={person.id}>
<Td width={75}>
<Checkbox
checked={selectedPeople.includes(person.id)}
onChange={() => onChange(person.id)}
/>
</Td>
<Td>
<div className="flex gap-3 items-center">
<Avatar name={person.fullName} />
<div>
<div className="text-sm">{person.fullName}</div>
<div className="text-xs text-txt-tertiary">
{person.primaryEmailAddress}
</div>
</div>
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
{isLoadingNext && <Spinner className="mt-3 mx-auto" />}
{hasNext && (
<Button
variant="tertiary"
onClick={() => loadNext(20)}
className="mx-auto"
icon={IconChevronDown}
type="button"
>
{sprintf(__("Show %s more"), people.length)}
</Button>
)}
</>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<4c3514470041046f80f410ea5a3275d6>>
* @generated SignedSource<<14890a4f10a8862a8aaf30ee1dc6f065>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -14,10 +14,10 @@ export type BulkRequestSignaturesInput = {
documentIds: ReadonlyArray<string>;
signatoryIds: ReadonlyArray<string>;
};
export type DocumentsPageSignatureMutation$variables = {
export type SignatureDocumentsDialogMutation$variables = {
input: BulkRequestSignaturesInput;
};
export type DocumentsPageSignatureMutation$data = {
export type SignatureDocumentsDialogMutation$data = {
readonly bulkRequestSignatures: {
readonly documentVersionSignatureEdges: ReadonlyArray<{
readonly node: {
@@ -27,9 +27,9 @@ export type DocumentsPageSignatureMutation$data = {
}>;
};
};
export type DocumentsPageSignatureMutation = {
response: DocumentsPageSignatureMutation$data;
variables: DocumentsPageSignatureMutation$variables;
export type SignatureDocumentsDialogMutation = {
response: SignatureDocumentsDialogMutation$data;
variables: SignatureDocumentsDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -100,7 +100,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "DocumentsPageSignatureMutation",
"name": "SignatureDocumentsDialogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -109,20 +109,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "DocumentsPageSignatureMutation",
"name": "SignatureDocumentsDialogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "a61a828bc67d8b569c38f4826b42d00f",
"cacheID": "4426d76c855d441caab5c932c2ceb2ba",
"id": null,
"metadata": {},
"name": "DocumentsPageSignatureMutation",
"name": "SignatureDocumentsDialogMutation",
"operationKind": "mutation",
"text": "mutation DocumentsPageSignatureMutation(\n $input: BulkRequestSignaturesInput!\n) {\n bulkRequestSignatures(input: $input) {\n documentVersionSignatureEdges {\n node {\n id\n state\n }\n }\n }\n}\n"
"text": "mutation SignatureDocumentsDialogMutation(\n $input: BulkRequestSignaturesInput!\n) {\n bulkRequestSignatures(input: $input) {\n documentVersionSignatureEdges {\n node {\n id\n state\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "1cb2b7dfa938b35cf75b09b51cdd2dca";
(node as any).hash = "1dbd9a7870416bf75d0a6fe44ebde408";
export default node;