From 00bba05faf59a2a534de01317f76d4726e91e5e9 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 23 Oct 2025 11:59:48 +0200 Subject: [PATCH] Hide accepted invitations Signed-off-by: Bryan Frimin --- CHANGELOG.md | 9 ++ .../OrganizationGraph_ViewQuery.graphql.ts | 106 ++++++++++-------- apps/console/src/layouts/MainLayout.tsx | 2 +- ...nizationSelectorPaginationQuery.graphql.ts | 14 ++- .../__generated__/MainLayoutQuery.graphql.ts | 12 +- ...out_OrganizationSelector_viewer.graphql.ts | 10 +- apps/console/src/pages/OrganizationsPage.tsx | 2 +- .../OrganizationsPageQuery.graphql.ts | 16 +-- .../src/pages/organizations/SettingsPage.tsx | 1 + ...SettingsInvitationsRefetchQuery.graphql.ts | 21 +++- ...SettingsPageInvitationsFragment.graphql.ts | 14 ++- pkg/authz/service.go | 6 +- pkg/coredata/invitation.go | 11 +- pkg/coredata/invitation_filter.go | 12 +- pkg/coredata/invitation_status.go | 23 +++- pkg/server/api/console/v1/schema.graphql | 5 +- pkg/server/api/console/v1/schema/schema.go | 83 +++++++++++--- pkg/server/api/console/v1/types/types.go | 2 +- pkg/server/api/console/v1/v1_resolver.go | 18 ++- 19 files changed, 255 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a9f1fa5..63fd42978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,19 @@ All notable changes to this project will be documented in this file. ## Unreleased +### Added + +- Add invitation filtering by multiple states (PENDING/ACCEPTED/EXPIRED) + +### Changed + +- Settings page now shows only pending and expired invitations (accepted invitations are hidden) + ### Fixed - Fix document classification not being passed when creating documents - Fix document classification changes not syncing to draft versions +- Fix organization invitations filter not being applied at database level ## [0.76.0] - 2025-10-22 diff --git a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts index aced2dbd8..34c67d5d1 100644 --- a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8c0884f06c22c11239816cecf638ebbd>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -89,50 +89,52 @@ v8 = { "name": "updatedAt", "storageKey": null }, -v9 = [ - { - "kind": "Literal", - "name": "first", - "value": 20 - }, - { - "kind": "Literal", - "name": "orderBy", - "value": { - "direction": "ASC", - "field": "CREATED_AT" - } - } -], +v9 = { + "kind": "Literal", + "name": "first", + "value": 20 +}, v10 = { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "CREATED_AT" + } +}, +v11 = [ + (v9/*: any*/), + (v10/*: any*/) +], +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "totalCount", "storageKey": null }, -v11 = { +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v12 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "role", "storageKey": null }, -v13 = { +v15 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v14 = { +v16 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -171,7 +173,7 @@ v14 = { ], "storageKey": null }, -v15 = { +v17 = { "kind": "ClientExtension", "selections": [ { @@ -183,8 +185,19 @@ v15 = { } ] }, -v16 = [ - "orderBy" +v18 = [ + { + "kind": "Literal", + "name": "filter", + "value": { + "statuses": [ + "PENDING", + "EXPIRED" + ] + } + }, + (v9/*: any*/), + (v10/*: any*/) ]; return { "fragment": { @@ -404,13 +417,13 @@ return { }, { "alias": null, - "args": (v9/*: any*/), + "args": (v11/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "memberships", "plural": false, "selections": [ - (v10/*: any*/), + (v12/*: any*/), { "alias": null, "args": null, @@ -428,7 +441,7 @@ return { "plural": false, "selections": [ (v2/*: any*/), - (v11/*: any*/), + (v13/*: any*/), { "alias": null, "args": null, @@ -436,25 +449,27 @@ return { "name": "emailAddress", "storageKey": null }, - (v12/*: any*/), + (v14/*: any*/), (v7/*: any*/), (v4/*: any*/) ], "storageKey": null }, - (v13/*: any*/) + (v15/*: any*/) ], "storageKey": null }, - (v14/*: any*/), - (v15/*: any*/) + (v16/*: any*/), + (v17/*: any*/) ], "storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v9/*: any*/), - "filters": (v16/*: any*/), + "args": (v11/*: any*/), + "filters": [ + "orderBy" + ], "handle": "connection", "key": "SettingsPageMemberships_memberships", "kind": "LinkedHandle", @@ -462,13 +477,13 @@ return { }, { "alias": null, - "args": (v9/*: any*/), + "args": (v18/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "invitations", "plural": false, "selections": [ - (v10/*: any*/), + (v12/*: any*/), { "alias": null, "args": null, @@ -487,8 +502,8 @@ return { "selections": [ (v2/*: any*/), (v5/*: any*/), - (v11/*: any*/), - (v12/*: any*/), + (v13/*: any*/), + (v14/*: any*/), { "alias": null, "args": null, @@ -515,19 +530,22 @@ return { ], "storageKey": null }, - (v13/*: any*/) + (v15/*: any*/) ], "storageKey": null }, - (v14/*: any*/), - (v15/*: any*/) + (v16/*: any*/), + (v17/*: any*/) ], - "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\",\"EXPIRED\"]},first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v9/*: any*/), - "filters": (v16/*: any*/), + "args": (v18/*: any*/), + "filters": [ + "orderBy", + "filter" + ], "handle": "connection", "key": "SettingsPageInvitations_invitations", "kind": "LinkedHandle", @@ -543,12 +561,12 @@ return { ] }, "params": { - "cacheID": "1f03968306005615bd5d2f70342db661", + "cacheID": "daa371367edee014ac05cb9712ce360d", "id": null, "metadata": {}, "name": "OrganizationGraph_ViewQuery", "operationKind": "query", - "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" } }; })(); diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index 401a75b5f..7b8aaeaed 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -87,7 +87,7 @@ const OrganizationSelectorFragment = graphql` endCursor } } - invitations(first: 1, filter: {status: PENDING}) { + invitations(first: 1, filter: {statuses: [PENDING]}) { totalCount } } diff --git a/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts index 53744d310..bec342b62 100644 --- a/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts +++ b/apps/console/src/layouts/__generated__/MainLayoutOrganizationSelectorPaginationQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<3a99643b92330af6920aac4c2286376f>> * @lightSyntaxTransform * @nogrep */ @@ -215,7 +215,9 @@ return { "kind": "Literal", "name": "filter", "value": { - "status": "PENDING" + "statuses": [ + "PENDING" + ] } }, { @@ -237,7 +239,7 @@ return { "storageKey": null } ], - "storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)" + "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" }, (v4/*: any*/) ], @@ -246,16 +248,16 @@ return { ] }, "params": { - "cacheID": "dbf01211a1e8b22f1d74fa9c2171231d", + "cacheID": "d51b49a50afe664ee9903c1bb265ab79", "id": null, "metadata": {}, "name": "MainLayoutOrganizationSelectorPaginationQuery", "operationKind": "query", - "text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {status: PENDING}) {\n totalCount\n }\n}\n" + "text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n" } }; })(); -(node as any).hash = "d1491a8228d455e2491724c03c7c30c1"; +(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf"; export default node; diff --git a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts index 9854f261c..b2a9c6600 100644 --- a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts +++ b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<1a7aa899a9b6251477122c87ae1c6431>> * @lightSyntaxTransform * @nogrep */ @@ -283,7 +283,9 @@ return { "kind": "Literal", "name": "filter", "value": { - "status": "PENDING" + "statuses": [ + "PENDING" + ] } }, { @@ -305,7 +307,7 @@ return { "storageKey": null } ], - "storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)" + "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" } ], "storageKey": null @@ -335,12 +337,12 @@ return { ] }, "params": { - "cacheID": "117a78fef8e7a8fe200baf1ebf2d2487", + "cacheID": "6de25e54419d82cb6465c903a0afdd78", "id": null, "metadata": {}, "name": "MainLayoutQuery", "operationKind": "query", - "text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {status: PENDING}) {\n totalCount\n }\n}\n" + "text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n" } }; })(); diff --git a/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts b/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts index 6125e750c..0b30c37a4 100644 --- a/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts +++ b/apps/console/src/layouts/__generated__/MainLayout_OrganizationSelector_viewer.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -189,7 +189,9 @@ return { "kind": "Literal", "name": "filter", "value": { - "status": "PENDING" + "statuses": [ + "PENDING" + ] } }, { @@ -211,7 +213,7 @@ return { "storageKey": null } ], - "storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)" + "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)" } ], "type": "Viewer", @@ -219,6 +221,6 @@ return { }; })(); -(node as any).hash = "d1491a8228d455e2491724c03c7c30c1"; +(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf"; export default node; diff --git a/apps/console/src/pages/OrganizationsPage.tsx b/apps/console/src/pages/OrganizationsPage.tsx index e2bd39989..e91987512 100644 --- a/apps/console/src/pages/OrganizationsPage.tsx +++ b/apps/console/src/pages/OrganizationsPage.tsx @@ -27,7 +27,7 @@ const OrganizationsPageQuery = graphql` } } } - invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {status: PENDING}) @connection(key: "OrganizationsPage_invitations") { + invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) @connection(key: "OrganizationsPage_invitations") { __id edges { node { diff --git a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts b/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts index f423eb302..f73796a4d 100644 --- a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts +++ b/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<580387b50cff64395ba9afd4c7454d20>> * @lightSyntaxTransform * @nogrep */ @@ -162,7 +162,9 @@ v8 = { "kind": "Literal", "name": "filter", "value": { - "status": "PENDING" + "statuses": [ + "PENDING" + ] } }, v9 = { @@ -309,7 +311,7 @@ return { "name": "__OrganizationsPage_invitations_connection", "plural": false, "selections": (v10/*: any*/), - "storageKey": "__OrganizationsPage_invitations_connection(filter:{\"status\":\"PENDING\"},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + "storageKey": "__OrganizationsPage_invitations_connection(filter:{\"statuses\":[\"PENDING\"]},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" } ], "storageKey": null @@ -361,7 +363,7 @@ return { "name": "invitations", "plural": false, "selections": (v10/*: any*/), - "storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, @@ -382,7 +384,7 @@ return { ] }, "params": { - "cacheID": "f8cd83b9b7ce0e1b43adc7aec05198bb", + "cacheID": "1b2e528133e56da643b9f8214a2f7192", "id": null, "metadata": { "connection": [ @@ -408,11 +410,11 @@ return { }, "name": "OrganizationsPageQuery", "operationKind": "query", - "text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {status: PENDING}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n" + "text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "d1957c93062d07020dc978b0f62bdf41"; +(node as any).hash = "ecd2534f36aa95ff7ff864ff22f09189"; export default node; diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 33c7a9967..9970ba993 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -159,6 +159,7 @@ const paginatedInvitationsFragment = graphql` last: $last before: $before orderBy: $order + filter: {statuses: [PENDING, EXPIRED]} ) @connection(key: "SettingsPageInvitations_invitations") { __id totalCount diff --git a/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts index 0a806de6c..bbd421fa1 100644 --- a/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts +++ b/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<33365519d19ebac5b95ae4c54652d49d>> + * @generated SignedSource<<39e11ee9cf99dc541aa5a8df259e8438>> * @lightSyntaxTransform * @nogrep */ @@ -112,6 +112,16 @@ v12 = { v13 = [ (v7/*: any*/), (v8/*: any*/), + { + "kind": "Literal", + "name": "filter", + "value": { + "statuses": [ + "PENDING", + "EXPIRED" + ] + } + }, (v9/*: any*/), (v10/*: any*/), { @@ -343,7 +353,8 @@ return { "alias": null, "args": (v13/*: any*/), "filters": [ - "orderBy" + "orderBy", + "filter" ], "handle": "connection", "key": "SettingsPageInvitations_invitations", @@ -360,16 +371,16 @@ return { ] }, "params": { - "cacheID": "0aa692d4815ddc0a17f76209ea0a5c4f", + "cacheID": "24b42a44e783a7f8499a568de2c8f9f4", "id": null, "metadata": {}, "name": "SettingsInvitationsRefetchQuery", "operationKind": "query", - "text": "query SettingsInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageInvitationsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + "text": "query SettingsInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageInvitationsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" } }; })(); -(node as any).hash = "f9a1ec38579cea21312ba0a20bb7394a"; +(node as any).hash = "b56157db731d3968bb825dd36375ddb7"; export default node; diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts index f4c8a7daa..619841913 100644 --- a/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts +++ b/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<09561c2c29459fc840773b414af6083a>> + * @generated SignedSource<<7ca4537a89a599fe5d2b7ecad753aa64>> * @lightSyntaxTransform * @nogrep */ @@ -117,6 +117,16 @@ return { { "alias": "invitations", "args": [ + { + "kind": "Literal", + "name": "filter", + "value": { + "statuses": [ + "PENDING", + "EXPIRED" + ] + } + }, { "kind": "Variable", "name": "orderBy", @@ -282,6 +292,6 @@ return { }; })(); -(node as any).hash = "f9a1ec38579cea21312ba0a20bb7394a"; +(node as any).hash = "b56157db731d3968bb825dd36375ddb7"; export default node; diff --git a/pkg/authz/service.go b/pkg/authz/service.go index 40f1f9bbb..9a46ecb7d 100644 --- a/pkg/authz/service.go +++ b/pkg/authz/service.go @@ -378,13 +378,14 @@ func (s *TenantAuthzService) GetInvitationsByOrganizationID( ctx context.Context, orgID gid.GID, cursor *page.Cursor[coredata.InvitationOrderField], + filter *coredata.InvitationFilter, ) (*page.Page[*coredata.Invitation, coredata.InvitationOrderField], error) { var invitations coredata.Invitations err := s.pg.WithConn( ctx, func(conn pg.Conn) error { - if err := invitations.LoadByOrganizationID(ctx, conn, s.scope, orgID, cursor); err != nil { + if err := invitations.LoadByOrganizationID(ctx, conn, s.scope, orgID, cursor, filter); err != nil { return fmt.Errorf("failed to load organization invitations: %w", err) } @@ -401,6 +402,7 @@ func (s *TenantAuthzService) GetInvitationsByOrganizationID( func (s *TenantAuthzService) CountOrganizationInvitations( ctx context.Context, orgID gid.GID, + filter *coredata.InvitationFilter, ) (int, error) { var count int err := s.pg.WithConn( @@ -408,7 +410,7 @@ func (s *TenantAuthzService) CountOrganizationInvitations( func(conn pg.Conn) error { var invitations coredata.Invitations var err error - count, err = invitations.CountByOrganizationID(ctx, conn, s.scope, orgID) + count, err = invitations.CountByOrganizationID(ctx, conn, s.scope, orgID, filter) return err }, ) diff --git a/pkg/coredata/invitation.go b/pkg/coredata/invitation.go index 82d1e5a7a..1f7bcbc78 100644 --- a/pkg/coredata/invitation.go +++ b/pkg/coredata/invitation.go @@ -297,6 +297,7 @@ func (i *Invitations) LoadByOrganizationID( scope Scoper, orgID gid.GID, cursor *page.Cursor[InvitationOrderField], + filter *InvitationFilter, ) error { query := ` SELECT @@ -319,14 +320,16 @@ WHERE organization_id = @organization_id AND %s AND %s + AND %s ` - query = fmt.Sprintf(query, scope.SQLFragment(), cursor.SQLFragment()) + query = fmt.Sprintf(query, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{ "organization_id": orgID, } maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, query, args) @@ -348,6 +351,7 @@ func (i *Invitations) CountByOrganizationID( conn pg.Conn, scope Scoper, orgID gid.GID, + filter *InvitationFilter, ) (int, error) { q := ` SELECT @@ -355,15 +359,16 @@ SELECT FROM authz_invitations WHERE - organization_id = @organization_id AND %s + organization_id = @organization_id AND %s AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) args := pgx.StrictNamedArgs{ "organization_id": orgID, } maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) diff --git a/pkg/coredata/invitation_filter.go b/pkg/coredata/invitation_filter.go index 69dd60aa7..67e7ad2c7 100644 --- a/pkg/coredata/invitation_filter.go +++ b/pkg/coredata/invitation_filter.go @@ -20,19 +20,19 @@ import ( type ( InvitationFilter struct { - status *InvitationStatus + statuses InvitationStatuses } ) -func NewInvitationFilter(status *InvitationStatus) *InvitationFilter { +func NewInvitationFilter(statuses []InvitationStatus) *InvitationFilter { return &InvitationFilter{ - status: status, + statuses: InvitationStatuses(statuses), } } func (f *InvitationFilter) SQLArguments() pgx.NamedArgs { return pgx.NamedArgs{ - "status": f.status, + "statuses": f.statuses, } } @@ -40,12 +40,12 @@ func (f *InvitationFilter) SQLFragment() string { return ` ( CASE - WHEN @status::text IS NOT NULL THEN + WHEN @statuses::text[] IS NOT NULL THEN (CASE WHEN accepted_at IS NOT NULL THEN 'ACCEPTED' WHEN expires_at < NOW() THEN 'EXPIRED' ELSE 'PENDING' - END) = @status::text + END) = ANY(@statuses::text[]) ELSE TRUE END )` diff --git a/pkg/coredata/invitation_status.go b/pkg/coredata/invitation_status.go index b17b30b80..26bc477f9 100644 --- a/pkg/coredata/invitation_status.go +++ b/pkg/coredata/invitation_status.go @@ -17,9 +17,13 @@ package coredata import ( "database/sql/driver" "fmt" + "strings" ) -type InvitationStatus string +type ( + InvitationStatus string + InvitationStatuses []InvitationStatus +) const ( InvitationStatusPending InvitationStatus = "PENDING" @@ -58,3 +62,20 @@ func (tcv *InvitationStatus) Scan(value any) error { func (tcv InvitationStatus) Value() (driver.Value, error) { return tcv.String(), nil } + +func (statuses InvitationStatuses) Value() (driver.Value, error) { + if len(statuses) == 0 { + return nil, nil + } + + var result strings.Builder + result.WriteString("{") + for i, status := range statuses { + if i > 0 { + result.WriteString(",") + } + result.WriteString(fmt.Sprintf("%q", status.String())) + } + result.WriteString("}") + return result.String(), nil +} diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index ff298dfc6..d29caabf1 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1508,7 +1508,7 @@ input InvitationOrder { } input InvitationFilter { - status: InvitationStatus + statuses: [InvitationStatus!] } input DocumentVersionFilter { @@ -3797,7 +3797,8 @@ input UpdateProcessingActivityInput { recipients: String @goField(omittable: true) location: String @goField(omittable: true) internationalTransfers: Boolean - transferSafeguards: ProcessingActivityTransferSafeguards @goField(omittable: true) + transferSafeguards: ProcessingActivityTransferSafeguards + @goField(omittable: true) retentionPeriod: String @goField(omittable: true) securityMeasures: String @goField(omittable: true) dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index d074d7dd0..510ecc0c6 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -10575,7 +10575,7 @@ input InvitationOrder { } input InvitationFilter { - status: InvitationStatus + statuses: [InvitationStatus!] } input DocumentVersionFilter { @@ -12864,7 +12864,8 @@ input UpdateProcessingActivityInput { recipients: String @goField(omittable: true) location: String @goField(omittable: true) internationalTransfers: Boolean - transferSafeguards: ProcessingActivityTransferSafeguards @goField(omittable: true) + transferSafeguards: ProcessingActivityTransferSafeguards + @goField(omittable: true) retentionPeriod: String @goField(omittable: true) securityMeasures: String @goField(omittable: true) dataProtectionImpactAssessment: ProcessingActivityDataProtectionImpactAssessment @@ -72331,20 +72332,20 @@ func (ec *executionContext) unmarshalInputInvitationFilter(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"status"} + fieldsInOrder := [...]string{"statuses"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { - case "status": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("status")) - data, err := ec.unmarshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus(ctx, v) + case "statuses": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("statuses")) + data, err := ec.unmarshalOInvitationStatus2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatusᚄ(ctx, v) if err != nil { return it, err } - it.Status = data + it.Statuses = data } } @@ -101735,32 +101736,78 @@ func (ec *executionContext) unmarshalOInvitationOrder2ᚖgithubᚗcomᚋgetprobo return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus(ctx context.Context, v any) (*coredata.InvitationStatus, error) { +func (ec *executionContext) unmarshalOInvitationStatus2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatusᚄ(ctx context.Context, v any) ([]coredata.InvitationStatus, error) { if v == nil { return nil, nil } - tmp, err := graphql.UnmarshalString(v) - res := unmarshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus[tmp] - return &res, graphql.ErrorOnPath(ctx, err) + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]coredata.InvitationStatus, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNInvitationStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil } -func (ec *executionContext) marshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus(ctx context.Context, sel ast.SelectionSet, v *coredata.InvitationStatus) graphql.Marshaler { +func (ec *executionContext) marshalOInvitationStatus2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatusᚄ(ctx context.Context, sel ast.SelectionSet, v []coredata.InvitationStatus) graphql.Marshaler { if v == nil { return graphql.Null } - _ = sel - _ = ctx - res := graphql.MarshalString(marshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus[*v]) - return res + 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.marshalNInvitationStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus(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 ( - unmarshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus = map[string]coredata.InvitationStatus{ + unmarshalOInvitationStatus2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatusᚄ = map[string]coredata.InvitationStatus{ "PENDING": coredata.InvitationStatusPending, "ACCEPTED": coredata.InvitationStatusAccepted, "EXPIRED": coredata.InvitationStatusExpired, } - marshalOInvitationStatus2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatus = map[coredata.InvitationStatus]string{ + marshalOInvitationStatus2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐInvitationStatusᚄ = map[coredata.InvitationStatus]string{ coredata.InvitationStatusPending: "PENDING", coredata.InvitationStatusAccepted: "ACCEPTED", coredata.InvitationStatusExpired: "EXPIRED", diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 5e365f3bf..bc04c6146 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1235,7 +1235,7 @@ type InvitationEdge struct { } type InvitationFilter struct { - Status *coredata.InvitationStatus `json:"status,omitempty"` + Statuses []coredata.InvitationStatus `json:"statuses,omitempty"` } type InvitationOrder struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 6aa32159b..8b6b316e6 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -911,8 +911,13 @@ func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invita func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) { switch obj.Resolver.(type) { case *organizationResolver: + invitationFilter := coredata.NewInvitationFilter(nil) + if obj.Filter != nil { + invitationFilter = coredata.NewInvitationFilter(obj.Filter.Statuses) + } + authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID()) - count, err := authzSvc.CountOrganizationInvitations(ctx, obj.ParentID) + count, err := authzSvc.CountOrganizationInvitations(ctx, obj.ParentID, invitationFilter) if err != nil { panic(fmt.Errorf("failed to count organization invitations: %w", err)) } @@ -925,7 +930,7 @@ func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *type invitationFilter := coredata.NewInvitationFilter(nil) if obj.Filter != nil { - invitationFilter = coredata.NewInvitationFilter(obj.Filter.Status) + invitationFilter = coredata.NewInvitationFilter(obj.Filter.Statuses) } count, err := r.authzSvc.CountUserInvitations(ctx, user.EmailAddress, invitationFilter) @@ -3647,8 +3652,13 @@ func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organ cursor := types.NewCursor(first, after, last, before, pageOrderBy) + invitationFilter := coredata.NewInvitationFilter(nil) + if filter != nil { + invitationFilter = coredata.NewInvitationFilter(filter.Statuses) + } + authzSvc := r.AuthzService(ctx, obj.ID.TenantID()) - page, err := authzSvc.GetInvitationsByOrganizationID(ctx, obj.ID, cursor) + page, err := authzSvc.GetInvitationsByOrganizationID(ctx, obj.ID, cursor, invitationFilter) if err != nil { panic(fmt.Errorf("cannot list invitations: %w", err)) } @@ -5363,7 +5373,7 @@ func (r *viewerResolver) Invitations(ctx context.Context, obj *types.Viewer, fir invitationFilter := coredata.NewInvitationFilter(nil) if filter != nil { - invitationFilter = coredata.NewInvitationFilter(filter.Status) + invitationFilter = coredata.NewInvitationFilter(filter.Statuses) } invitations, err := r.authzSvc.GetUserInvitations(ctx, user.EmailAddress, cursor, invitationFilter)