From 06c05db3a4f7d12e11d9394178c08b253af4d900 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Tue, 21 Oct 2025 11:49:03 +0200 Subject: [PATCH] Add people signature filtering Signed-off-by: Bryan Frimin --- .../DocumentGraphNodeQuery.graphql.ts | 19 +- .../documents/DocumentDetailPage.tsx | 3 +- ...umentDetailPageDocumentFragment.graphql.ts | 10 +- .../documents/tabs/DocumentSignaturesTab.tsx | 129 ++++++- ...cumentSignaturesTabRefetchQuery.graphql.ts | 330 ++++++++++++++++++ .../DocumentSignaturesTab_version.graphql.ts | 247 +++++++++++++ pkg/coredata/document_version_signature.go | 5 +- .../document_version_signature_filter.go | 48 +++ .../document_version_signature_state.go | 17 +- pkg/probo/document_service.go | 3 +- pkg/server/api/console/v1/schema.graphql | 5 + pkg/server/api/console/v1/schema/schema.go | 143 +++++++- pkg/server/api/console/v1/types/types.go | 4 + pkg/server/api/console/v1/v1_resolver.go | 10 +- 14 files changed, 943 insertions(+), 30 deletions(-) create mode 100644 apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTabRefetchQuery.graphql.ts create mode 100644 apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts create mode 100644 pkg/coredata/document_version_signature_filter.go diff --git a/apps/console/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts index ce9dd96ac..ec521db01 100644 --- a/apps/console/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<4d39076389b55a86a116d8115ac22fca>> + * @generated SignedSource<<3b517368f8d0fc79e1c5c60e56c00b4d>> * @lightSyntaxTransform * @nogrep */ @@ -469,7 +469,18 @@ return { { "alias": null, "args": (v11/*: any*/), - "filters": null, + "filters": [ + "filter" + ], + "handle": "connection", + "key": "DocumentSignaturesTab_signatures", + "kind": "LinkedHandle", + "name": "signatures" + }, + { + "alias": null, + "args": (v11/*: any*/), + "filters": [], "handle": "connection", "key": "DocumentDetailPage_signatures", "kind": "LinkedHandle", @@ -507,12 +518,12 @@ return { ] }, "params": { - "cacheID": "6d2e59057d037990aeb917bb36d8234f", + "cacheID": "d77cc1c3f05f4be15b7c50b90b8a4704", "id": null, "metadata": {}, "name": "DocumentGraphNodeQuery", "operationKind": "query", - "text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentDetailPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentControlsTabFragment on Document {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment DocumentDetailPageDocumentFragment on Document {\n id\n title\n documentType\n owner {\n id\n fullName\n }\n ...DocumentControlsTabFragment\n controlsInfo: controls(first: 0) {\n totalCount\n }\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 500) {\n edges {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesTab_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DocumentSignaturesTab_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n" + "text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentDetailPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentControlsTabFragment on Document {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment DocumentDetailPageDocumentFragment on Document {\n id\n title\n documentType\n owner {\n id\n fullName\n }\n ...DocumentControlsTabFragment\n controlsInfo: controls(first: 0) {\n totalCount\n }\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n ...DocumentSignaturesTab_version\n signatures(first: 500) {\n edges {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesTab_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DocumentSignaturesTab_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment DocumentSignaturesTab_version on DocumentVersion {\n id\n status\n signatures(first: 500) {\n edges {\n node {\n id\n state\n signedBy {\n id\n fullName\n primaryEmailAddress\n }\n ...DocumentSignaturesTab_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n" } }; })(); diff --git a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx index 369695e25..9170deccc 100644 --- a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -101,8 +101,9 @@ const documentFragment = graphql` publishedAt version updatedAt + ...DocumentSignaturesTab_version signatures(first: 500) - @connection(key: "DocumentDetailPage_signatures") { + @connection(key: "DocumentDetailPage_signatures", filters: []) { __id edges { node { diff --git a/apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts b/apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts index e7371afe5..a52473057 100644 --- a/apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts +++ b/apps/console/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<1b07d7b3b18267f31c2b7ec7950edcb6>> + * @generated SignedSource<<79bf0f8ac3d41ba5c456ddae462e2c6d>> * @lightSyntaxTransform * @nogrep */ @@ -47,6 +47,7 @@ export type DocumentDetailPageDocumentFragment$data = { readonly status: DocumentStatus; readonly updatedAt: any; readonly version: number; + readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_version">; }; }>; }; @@ -263,6 +264,11 @@ return { "name": "updatedAt", "storageKey": null }, + { + "args": null, + "kind": "FragmentSpread", + "name": "DocumentSignaturesTab_version" + }, { "alias": "signatures", "args": null, @@ -344,6 +350,6 @@ return { }; })(); -(node as any).hash = "ffd5764324ccdcfe201e937980e75253"; +(node as any).hash = "ce3a8422f4391952a8ad839ff874c5cf"; export default node; diff --git a/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx b/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx index 33699e5f8..7f61fd7b7 100644 --- a/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx +++ b/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx @@ -2,14 +2,15 @@ import { Avatar, Badge, Button, + Checkbox, IconCircleCheck, IconClock, Spinner, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; -import { Suspense } from "react"; +import { Suspense, useState, useEffect } from "react"; import type { ItemOf, NodeOf } from "/types"; -import { graphql, useFragment } from "react-relay"; +import { graphql, useFragment, useRefetchableFragment } from "react-relay"; import { usePeople } from "/hooks/graph/PeopleGraph.ts"; import { useOrganizationId } from "/hooks/useOrganizationId.ts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts.ts"; @@ -17,31 +18,118 @@ import { sprintf } from "@probo/helpers"; import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql"; import { useOutletContext } from "react-router"; import type { DocumentSignaturesTab_signature$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_signature.graphql.ts"; +import type { DocumentSignaturesTab_version$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts"; type Version = NodeOf; +const versionFragment = graphql` + fragment DocumentSignaturesTab_version on DocumentVersion + @refetchable(queryName: "DocumentSignaturesTabRefetchQuery") + @argumentDefinitions( + count: { type: "Int", defaultValue: 500 } + cursor: { type: "CursorKey" } + signatureFilter: { type: "DocumentVersionSignatureFilter" } + ) { + id + status + signatures(first: $count, after: $cursor, filter: $signatureFilter) + @connection(key: "DocumentSignaturesTab_signatures", filters: ["filter"]) { + __id + edges { + node { + id + state + signedBy { + id + fullName + primaryEmailAddress + } + ...DocumentSignaturesTab_signature + } + } + } + } +`; + +type SignatureState = "REQUESTED" | "SIGNED"; + export default function DocumentSignaturesTab() { - const { version } = useOutletContext<{ version: Version }>(); - if (!version) { + const { version: versionFromContext } = useOutletContext<{ version: Version }>(); + const [selectedStates, setSelectedStates] = useState([]); + const { __ } = useTranslate(); + + if (!versionFromContext) { return null; } + const toggleState = (state: SignatureState) => { + setSelectedStates((prev) => + prev.includes(state) + ? prev.filter((s) => s !== state) + : [...prev, state] + ); + }; + return ( - }> - - +
+
+ + {__("Filter by state:")} + +
+ toggleState("REQUESTED")} + /> + toggleState("REQUESTED")} + > + {__("Requested")} + +
+
+ toggleState("SIGNED")} + /> + toggleState("SIGNED")} + > + {__("Signed")} + +
+
+ }> + + +
); } -function SignatureList(props: { version: Version }) { - const signatures = props.version.signatures?.edges?.map((edge) => edge.node) ?? []; +function SignatureList(props: { version: Version; selectedStates: SignatureState[] }) { + const [version, refetch] = useRefetchableFragment( + versionFragment, + props.version + ); + const signatures = version.signatures?.edges?.map((edge) => edge.node) ?? []; const { __ } = useTranslate(); const signatureMap = new Map(signatures.map((s) => [s.signedBy.id, s])); const organizationId = useOrganizationId(); const people = usePeople(organizationId, { excludeContractEnded: true }); - const signable = props.version.status === "PUBLISHED"; + const signable = version.status === "PUBLISHED"; - if (!props.version.signatures) { + // Refetch when filter changes + useEffect(() => { + const filter = props.selectedStates.length > 0 + ? { states: props.selectedStates } + : null; + + refetch({ signatureFilter: filter }); + }, [JSON.stringify(props.selectedStates), refetch]); + + if (!version.signatures) { return (
{__("Loading signatures...")} @@ -57,15 +145,28 @@ function SignatureList(props: { version: Version }) { ); } + // When a filter is active, only show people who have signatures in the filtered results + const filteredPeople = props.selectedStates.length > 0 + ? people.filter((p) => signatureMap.has(p.id)) + : people; + + if (filteredPeople.length === 0 && props.selectedStates.length > 0) { + return ( +
+ {__("No signatures match the selected filters")} +
+ ); + } + return (
- {people.map((p) => ( + {filteredPeople.map((p) => ( ))} diff --git a/apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTabRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTabRefetchQuery.graphql.ts new file mode 100644 index 000000000..f9130aff0 --- /dev/null +++ b/apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTabRefetchQuery.graphql.ts @@ -0,0 +1,330 @@ +/** + * @generated SignedSource<<6e738ec6de4916bb47e6698ef348a702>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type DocumentVersionSignatureState = "REQUESTED" | "SIGNED"; +export type DocumentVersionSignatureFilter = { + states?: ReadonlyArray | null | undefined; +}; +export type DocumentSignaturesTabRefetchQuery$variables = { + count?: number | null | undefined; + cursor?: any | null | undefined; + id: string; + signatureFilter?: DocumentVersionSignatureFilter | null | undefined; +}; +export type DocumentSignaturesTabRefetchQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_version">; + }; +}; +export type DocumentSignaturesTabRefetchQuery = { + response: DocumentSignaturesTabRefetchQuery$data; + variables: DocumentSignaturesTabRefetchQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": 500, + "kind": "LocalArgument", + "name": "count" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "signatureFilter" +}, +v4 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v7 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "filter", + "variableName": "signatureFilter" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "count" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "DocumentSignaturesTabRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + { + "kind": "Variable", + "name": "count", + "variableName": "count" + }, + { + "kind": "Variable", + "name": "cursor", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "signatureFilter", + "variableName": "signatureFilter" + } + ], + "kind": "FragmentSpread", + "name": "DocumentSignaturesTab_version" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v3/*: any*/), + (v2/*: any*/) + ], + "kind": "Operation", + "name": "DocumentSignaturesTabRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v5/*: any*/), + (v6/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": "DocumentVersionSignatureConnection", + "kind": "LinkedField", + "name": "signatures", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentVersionSignatureEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentVersionSignature", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "signedBy", + "plural": false, + "selections": [ + (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "signedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "requestedAt", + "storageKey": null + }, + (v5/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v7/*: any*/), + "filters": [ + "filter" + ], + "handle": "connection", + "key": "DocumentSignaturesTab_signatures", + "kind": "LinkedHandle", + "name": "signatures" + } + ], + "type": "DocumentVersion", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3f2497c26794429a940d920be7459a16", + "id": null, + "metadata": {}, + "name": "DocumentSignaturesTabRefetchQuery", + "operationKind": "query", + "text": "query DocumentSignaturesTabRefetchQuery(\n $count: Int = 500\n $cursor: CursorKey\n $signatureFilter: DocumentVersionSignatureFilter\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...DocumentSignaturesTab_version_1vp7QE\n id\n }\n}\n\nfragment DocumentSignaturesTab_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment DocumentSignaturesTab_version_1vp7QE on DocumentVersion {\n id\n status\n signatures(first: $count, after: $cursor, filter: $signatureFilter) {\n edges {\n node {\n id\n state\n signedBy {\n id\n fullName\n primaryEmailAddress\n }\n ...DocumentSignaturesTab_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "8692b71c860fba2b26a641a637920930"; + +export default node; diff --git a/apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts b/apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts new file mode 100644 index 000000000..a728332b7 --- /dev/null +++ b/apps/console/src/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts @@ -0,0 +1,247 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type DocumentStatus = "DRAFT" | "PUBLISHED"; +export type DocumentVersionSignatureState = "REQUESTED" | "SIGNED"; +import { FragmentRefs } from "relay-runtime"; +export type DocumentSignaturesTab_version$data = { + readonly id: string; + readonly signatures: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly signedBy: { + readonly fullName: string; + readonly id: string; + readonly primaryEmailAddress: string; + }; + readonly state: DocumentVersionSignatureState; + readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_signature">; + }; + }>; + }; + readonly status: DocumentStatus; + readonly " $fragmentType": "DocumentSignaturesTab_version"; +}; +export type DocumentSignaturesTab_version$key = { + readonly " $data"?: DocumentSignaturesTab_version$data; + readonly " $fragmentSpreads": FragmentRefs<"DocumentSignaturesTab_version">; +}; + +import DocumentSignaturesTabRefetchQuery_graphql from './DocumentSignaturesTabRefetchQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "signatures" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": 500, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "signatureFilter" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": "count", + "cursor": "cursor", + "direction": "forward", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "count", + "cursor": "cursor" + }, + "backward": null, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": DocumentSignaturesTabRefetchQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "DocumentSignaturesTab_version", + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": "signatures", + "args": [ + { + "kind": "Variable", + "name": "filter", + "variableName": "signatureFilter" + } + ], + "concreteType": "DocumentVersionSignatureConnection", + "kind": "LinkedField", + "name": "__DocumentSignaturesTab_signatures_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentVersionSignatureEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentVersionSignature", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "signedBy", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "primaryEmailAddress", + "storageKey": null + } + ], + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "DocumentSignaturesTab_signature" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + } + ], + "type": "DocumentVersion", + "abstractKey": null +}; +})(); + +(node as any).hash = "8692b71c860fba2b26a641a637920930"; + +export default node; diff --git a/pkg/coredata/document_version_signature.go b/pkg/coredata/document_version_signature.go index 7da92edfa..0d02e036f 100644 --- a/pkg/coredata/document_version_signature.go +++ b/pkg/coredata/document_version_signature.go @@ -203,6 +203,7 @@ func (pvss *DocumentVersionSignatures) LoadByDocumentVersionID( scope Scoper, documentVersionID gid.GID, cursor *page.Cursor[DocumentVersionSignatureOrderField], + filter *DocumentVersionSignatureFilter, ) error { q := ` SELECT @@ -220,13 +221,15 @@ WHERE %s AND document_version_id = @document_version_id AND %s + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"document_version_id": documentVersionID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { diff --git a/pkg/coredata/document_version_signature_filter.go b/pkg/coredata/document_version_signature_filter.go new file mode 100644 index 000000000..ed69b6ff8 --- /dev/null +++ b/pkg/coredata/document_version_signature_filter.go @@ -0,0 +1,48 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "github.com/jackc/pgx/v5" +) + +type ( + DocumentVersionSignatureFilter struct { + states DocumentVersionSignatureStates + } +) + +func NewDocumentVersionSignatureFilter(states []DocumentVersionSignatureState) *DocumentVersionSignatureFilter { + return &DocumentVersionSignatureFilter{ + states: DocumentVersionSignatureStates(states), + } +} + +func (f *DocumentVersionSignatureFilter) SQLArguments() pgx.StrictNamedArgs { + return pgx.StrictNamedArgs{ + "states": f.states, + } +} + +func (f *DocumentVersionSignatureFilter) SQLFragment() string { + return ` +( + CASE + WHEN @states::policy_version_signature_state[] IS NOT NULL THEN + state = ANY(@states::policy_version_signature_state[]) + ELSE TRUE + END +)` +} diff --git a/pkg/coredata/document_version_signature_state.go b/pkg/coredata/document_version_signature_state.go index c4628ca1f..67e835da9 100644 --- a/pkg/coredata/document_version_signature_state.go +++ b/pkg/coredata/document_version_signature_state.go @@ -17,10 +17,12 @@ package coredata import ( "database/sql/driver" "fmt" + "strings" ) type ( - DocumentVersionSignatureState string + DocumentVersionSignatureState string + DocumentVersionSignatureStates []DocumentVersionSignatureState ) const ( @@ -74,3 +76,16 @@ func (pvs *DocumentVersionSignatureState) Scan(value any) error { func (pvs DocumentVersionSignatureState) Value() (driver.Value, error) { return pvs.String(), nil } + +func (states DocumentVersionSignatureStates) Value() (driver.Value, error) { + var result strings.Builder + result.WriteString("{") + for i, state := range states { + if i > 0 { + result.WriteString(",") + } + result.WriteString(fmt.Sprintf("%q", state.String())) + } + result.WriteString("}") + return result.String(), nil +} diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index 091933591..e8406f283 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -715,13 +715,14 @@ func (s *DocumentService) ListSignatures( ctx context.Context, documentVersionID gid.GID, cursor *page.Cursor[coredata.DocumentVersionSignatureOrderField], + filter *coredata.DocumentVersionSignatureFilter, ) (*page.Page[*coredata.DocumentVersionSignature, coredata.DocumentVersionSignatureOrderField], error) { var documentVersionSignatures coredata.DocumentVersionSignatures err := s.svc.pg.WithConn( ctx, func(conn pg.Conn) error { - return documentVersionSignatures.LoadByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID, cursor) + return documentVersionSignatures.LoadByDocumentVersionID(ctx, conn, s.svc.scope, documentVersionID, cursor, filter) }, ) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 0f2289b6b..f2ca1a487 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -4182,6 +4182,7 @@ type DocumentVersion implements Node { last: Int before: CursorKey orderBy: DocumentVersionSignatureOrder + filter: DocumentVersionSignatureFilter ): DocumentVersionSignatureConnection! @goField(forceResolver: true) publishedAt: Datetime @@ -4204,6 +4205,10 @@ input DocumentVersionSignatureOrder { direction: OrderDirection! } +input DocumentVersionSignatureFilter { + states: [DocumentVersionSignatureState!] +} + enum DocumentVersionSignatureState @goModel( model: "github.com/getprobo/probo/pkg/coredata.DocumentVersionSignatureState" diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 824bb15cf..ed245961a 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -640,7 +640,7 @@ type ComplexityRoot struct { ID func(childComplexity int) int Owner func(childComplexity int) int PublishedAt func(childComplexity int) int - Signatures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder) int + Signatures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder, filter *types.DocumentVersionSignatureFilter) int Status func(childComplexity int) int Title func(childComplexity int) int UpdatedAt func(childComplexity int) int @@ -1700,7 +1700,7 @@ type DocumentVersionResolver interface { Document(ctx context.Context, obj *types.DocumentVersion) (*types.Document, error) Owner(ctx context.Context, obj *types.DocumentVersion) (*types.People, error) - Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder) (*types.DocumentVersionSignatureConnection, error) + Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder, filter *types.DocumentVersionSignatureFilter) (*types.DocumentVersionSignatureConnection, error) } type DocumentVersionSignatureResolver interface { DocumentVersion(ctx context.Context, obj *types.DocumentVersionSignature) (*types.DocumentVersion, error) @@ -3675,7 +3675,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.DocumentVersion.Signatures(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.DocumentVersionSignatureOrder)), true + return e.complexity.DocumentVersion.Signatures(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.DocumentVersionSignatureOrder), args["filter"].(*types.DocumentVersionSignatureFilter)), true case "DocumentVersion.status": if e.complexity.DocumentVersion.Status == nil { @@ -8876,6 +8876,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDocumentOrder, ec.unmarshalInputDocumentVersionFilter, ec.unmarshalInputDocumentVersionOrder, + ec.unmarshalInputDocumentVersionSignatureFilter, ec.unmarshalInputDocumentVersionSignatureOrder, ec.unmarshalInputEvidenceOrder, ec.unmarshalInputExportDocumentVersionPDFInput, @@ -13232,6 +13233,7 @@ type DocumentVersion implements Node { last: Int before: CursorKey orderBy: DocumentVersionSignatureOrder + filter: DocumentVersionSignatureFilter ): DocumentVersionSignatureConnection! @goField(forceResolver: true) publishedAt: Datetime @@ -13254,6 +13256,10 @@ input DocumentVersionSignatureOrder { direction: OrderDirection! } +input DocumentVersionSignatureFilter { + states: [DocumentVersionSignatureState!] +} + enum DocumentVersionSignatureState @goModel( model: "github.com/getprobo/probo/pkg/coredata.DocumentVersionSignatureState" @@ -14450,6 +14456,11 @@ func (ec *executionContext) field_DocumentVersion_signatures_args(ctx context.Co return nil, err } args["orderBy"] = arg4 + arg5, err := ec.field_DocumentVersion_signatures_argsFilter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["filter"] = arg5 return args, nil } func (ec *executionContext) field_DocumentVersion_signatures_argsFirst( @@ -14517,6 +14528,19 @@ func (ec *executionContext) field_DocumentVersion_signatures_argsOrderBy( return zeroVal, nil } +func (ec *executionContext) field_DocumentVersion_signatures_argsFilter( + ctx context.Context, + rawArgs map[string]any, +) (*types.DocumentVersionSignatureFilter, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + if tmp, ok := rawArgs["filter"]; ok { + return ec.unmarshalODocumentVersionSignatureFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDocumentVersionSignatureFilter(ctx, tmp) + } + + var zeroVal *types.DocumentVersionSignatureFilter + return zeroVal, nil +} + func (ec *executionContext) field_Document_controls_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -33186,7 +33210,7 @@ func (ec *executionContext) _DocumentVersion_signatures(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.DocumentVersion().Signatures(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.DocumentVersionSignatureOrder)) + return ec.resolvers.DocumentVersion().Signatures(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.DocumentVersionSignatureOrder), fc.Args["filter"].(*types.DocumentVersionSignatureFilter)) }) if err != nil { ec.Error(ctx, err) @@ -71761,6 +71785,33 @@ func (ec *executionContext) unmarshalInputDocumentVersionOrder(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputDocumentVersionSignatureFilter(ctx context.Context, obj any) (types.DocumentVersionSignatureFilter, error) { + var it types.DocumentVersionSignatureFilter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"states"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "states": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("states")) + data, err := ec.unmarshalODocumentVersionSignatureState2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureStateᚄ(ctx, v) + if err != nil { + return it, err + } + it.States = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDocumentVersionSignatureOrder(ctx context.Context, obj any) (types.DocumentVersionSignatureOrder, error) { var it types.DocumentVersionSignatureOrder asMap := map[string]any{} @@ -101215,6 +101266,14 @@ func (ec *executionContext) unmarshalODocumentVersionOrder2ᚖgithubᚗcomᚋget return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalODocumentVersionSignatureFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDocumentVersionSignatureFilter(ctx context.Context, v any) (*types.DocumentVersionSignatureFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputDocumentVersionSignatureFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalODocumentVersionSignatureOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDocumentVersionSignatureOrder(ctx context.Context, v any) (*types.DocumentVersionSignatureOrder, error) { if v == nil { return nil, nil @@ -101223,6 +101282,82 @@ func (ec *executionContext) unmarshalODocumentVersionSignatureOrder2ᚖgithubᚗ return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalODocumentVersionSignatureState2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureStateᚄ(ctx context.Context, v any) ([]coredata.DocumentVersionSignatureState, error) { + if v == nil { + return nil, nil + } + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]coredata.DocumentVersionSignatureState, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNDocumentVersionSignatureState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureState(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalODocumentVersionSignatureState2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureStateᚄ(ctx context.Context, sel ast.SelectionSet, v []coredata.DocumentVersionSignatureState) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNDocumentVersionSignatureState2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureState(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +var ( + unmarshalODocumentVersionSignatureState2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureStateᚄ = map[string]coredata.DocumentVersionSignatureState{ + "REQUESTED": coredata.DocumentVersionSignatureStateRequested, + "SIGNED": coredata.DocumentVersionSignatureStateSigned, + } + marshalODocumentVersionSignatureState2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐDocumentVersionSignatureStateᚄ = map[coredata.DocumentVersionSignatureState]string{ + coredata.DocumentVersionSignatureStateRequested: "REQUESTED", + coredata.DocumentVersionSignatureStateSigned: "SIGNED", + } +) + func (ec *executionContext) unmarshalODuration2ᚖtimeᚐDuration(ctx context.Context, v any) (*time.Duration, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index bf17cb70e..cafb16c47 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1088,6 +1088,10 @@ type DocumentVersionSignatureEdge struct { Node *DocumentVersionSignature `json:"node"` } +type DocumentVersionSignatureFilter struct { + States []coredata.DocumentVersionSignatureState `json:"states,omitempty"` +} + type DocumentVersionSignatureOrder struct { Field coredata.DocumentVersionSignatureOrderField `json:"field"` Direction page.OrderDirection `json:"direction"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 8c7d7b96d..a2bc9055f 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -677,7 +677,7 @@ func (r *documentVersionResolver) Owner(ctx context.Context, obj *types.Document } // Signatures is the resolver for the signatures field. -func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder) (*types.DocumentVersionSignatureConnection, error) { +func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.DocumentVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionSignatureOrder, filter *types.DocumentVersionSignatureFilter) (*types.DocumentVersionSignatureConnection, error) { prb := r.ProboService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.DocumentVersionSignatureOrderField]{ @@ -691,9 +691,15 @@ func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.Doc } } + var signatureStates []coredata.DocumentVersionSignatureState + if filter != nil && filter.States != nil { + signatureStates = filter.States + } + signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates) + cursor := types.NewCursor(first, after, last, before, pageOrderBy) - page, err := prb.Documents.ListSignatures(ctx, obj.ID, cursor) + page, err := prb.Documents.ListSignatures(ctx, obj.ID, cursor, signatureFilter) if err != nil { panic(fmt.Errorf("cannot list document version signatures: %w", err)) }