From f8accc2d40b47a10ea38b4135e68d09a8e8cf74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 5 Jan 2026 19:50:14 +0100 Subject: [PATCH] Fix invitations totalCount update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../iam/InvitationListFragment.graphql.ts | 12 +- ...tationListFragment_RefetchQuery.graphql.ts | 15 +- ...eUserDialog_currentRoleFragment.graphql.ts | 60 ------ .../iam/MembersPageQuery.graphql.ts | 192 +++++++++--------- ...e_invitationsTotalCountFragment.graphql.ts | 42 ++++ apps/console/src/environments.ts | 2 +- .../organizations/settings/MembersPage.tsx | 15 +- .../settings/_components/InvitationList.tsx | 9 +- .../_components/InvitationListItem.tsx | 16 +- .../settings/_components/InviteUserDialog.tsx | 57 +++--- pkg/server/api/connect/v1/v1_resolver.go | 4 +- 11 files changed, 209 insertions(+), 215 deletions(-) delete mode 100644 apps/console/src/__generated__/iam/InviteUserDialog_currentRoleFragment.graphql.ts create mode 100644 apps/console/src/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql.ts diff --git a/apps/console/src/__generated__/iam/InvitationListFragment.graphql.ts b/apps/console/src/__generated__/iam/InvitationListFragment.graphql.ts index b6150fdd9..403558ab4 100644 --- a/apps/console/src/__generated__/iam/InvitationListFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/InvitationListFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<26877e6a0dc423e87108c5b2ea1b11a3>> * @lightSyntaxTransform * @nogrep */ @@ -20,7 +20,6 @@ export type InvitationListFragment$data = { readonly " $fragmentSpreads": FragmentRefs<"InvitationListItemFragment">; }; }>; - readonly totalCount: number | null | undefined; }; readonly " $fragmentType": "InvitationListFragment"; }; @@ -123,13 +122,6 @@ return { "name": "__InvitationListFragment_invitations_connection", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "totalCount", - "storageKey": null - }, { "kind": "RequiredField", "field": { @@ -239,6 +231,6 @@ return { }; })(); -(node as any).hash = "d6f8750bb89b233418c3a66669c4aa23"; +(node as any).hash = "224d655216572cb18f193cdfcae64172"; export default node; diff --git a/apps/console/src/__generated__/iam/InvitationListFragment_RefetchQuery.graphql.ts b/apps/console/src/__generated__/iam/InvitationListFragment_RefetchQuery.graphql.ts index f18069ca1..e5ae7f5f3 100644 --- a/apps/console/src/__generated__/iam/InvitationListFragment_RefetchQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/InvitationListFragment_RefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<23d03caf79ac153cc19aae8e87c38a27>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -198,13 +198,6 @@ return { "name": "invitations", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "totalCount", - "storageKey": null - }, { "alias": null, "args": null, @@ -373,16 +366,16 @@ return { ] }, "params": { - "cacheID": "2f419e2fcb92f5a188dcd35ff5661fd7", + "cacheID": "afc5598fd9e211b2e7af8ac8bce27511", "id": null, "metadata": {}, "name": "InvitationListFragment_RefetchQuery", "operationKind": "query", - "text": "query InvitationListFragment_RefetchQuery(\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 ...InvitationListFragment_16fISc\n id\n }\n}\n\nfragment InvitationListFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n" + "text": "query InvitationListFragment_RefetchQuery(\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 ...InvitationListFragment_16fISc\n id\n }\n}\n\nfragment InvitationListFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n" } }; })(); -(node as any).hash = "d6f8750bb89b233418c3a66669c4aa23"; +(node as any).hash = "224d655216572cb18f193cdfcae64172"; export default node; diff --git a/apps/console/src/__generated__/iam/InviteUserDialog_currentRoleFragment.graphql.ts b/apps/console/src/__generated__/iam/InviteUserDialog_currentRoleFragment.graphql.ts deleted file mode 100644 index b9981c585..000000000 --- a/apps/console/src/__generated__/iam/InviteUserDialog_currentRoleFragment.graphql.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ReaderFragment } from 'relay-runtime'; -export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; -import { FragmentRefs } from "relay-runtime"; -export type InviteUserDialog_currentRoleFragment$data = { - readonly viewerMembership: { - readonly role: MembershipRole; - }; - readonly " $fragmentType": "InviteUserDialog_currentRoleFragment"; -}; -export type InviteUserDialog_currentRoleFragment$key = { - readonly " $data"?: InviteUserDialog_currentRoleFragment$data; - readonly " $fragmentSpreads": FragmentRefs<"InviteUserDialog_currentRoleFragment">; -}; - -const node: ReaderFragment = { - "argumentDefinitions": [], - "kind": "Fragment", - "metadata": null, - "name": "InviteUserDialog_currentRoleFragment", - "selections": [ - { - "kind": "RequiredField", - "field": { - "alias": null, - "args": null, - "concreteType": "Membership", - "kind": "LinkedField", - "name": "viewerMembership", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "role", - "storageKey": null - } - ], - "storageKey": null - }, - "action": "THROW" - } - ], - "type": "Organization", - "abstractKey": null -}; - -(node as any).hash = "167528734d4ad650128d5ed8f201c97e"; - -export default node; diff --git a/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts b/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts index 18592f833..17d346ff1 100644 --- a/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/MembersPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<4d9333564d383b3af80a4d6c2ce68277>> * @lightSyntaxTransform * @nogrep */ @@ -18,12 +18,13 @@ export type MembersPageQuery$data = { readonly __typename: "Organization"; readonly canInviteUser: boolean; readonly invitations: { + readonly $updatableFragmentSpreads: FragmentRefs<"MembersPage_invitationsTotalCountFragment">; readonly totalCount: number | null | undefined; }; readonly members: { readonly totalCount: number | null | undefined; }; - readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment" | "InviteUserDialog_currentRoleFragment" | "MemberListFragment">; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment" | "MemberListFragment">; } | { // This will never be '%other', but we need some // value in case none of the concrete values match. @@ -58,41 +59,6 @@ v2 = { "storageKey": null }, v3 = { - "kind": "Literal", - "name": "first", - "value": 20 -}, -v4 = { - "direction": "ASC", - "field": "CREATED_AT" -}, -v5 = [ - (v3/*: any*/), - { - "kind": "Literal", - "name": "order", - "value": (v4/*: any*/) - } -], -v6 = [ - (v3/*: any*/), - { - "kind": "Literal", - "name": "orderBy", - "value": (v4/*: any*/) - } -], -v7 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "totalCount", - "storageKey": null -}, -v8 = [ - (v7/*: any*/) -], -v9 = { "alias": "canInviteUser", "args": [ { @@ -105,49 +71,81 @@ v9 = { "name": "permission", "storageKey": "permission(action:\"iam:invitation:create\")" }, -v10 = { +v4 = { + "kind": "Literal", + "name": "first", + "value": 20 +}, +v5 = { + "direction": "ASC", + "field": "CREATED_AT" +}, +v6 = [ + (v4/*: any*/), + { + "kind": "Literal", + "name": "order", + "value": (v5/*: any*/) + } +], +v7 = [ + (v4/*: any*/), + { + "kind": "Literal", + "name": "orderBy", + "value": (v5/*: any*/) + } +], +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null +}, +v9 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v11 = { +v10 = { "alias": null, "args": null, "kind": "ScalarField", "name": "role", "storageKey": null }, -v12 = { +v11 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v13 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "email", "storageKey": null }, -v14 = { +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v15 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v16 = { +v15 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -186,7 +184,7 @@ v16 = { ], "storageKey": null }, -v17 = { +v16 = { "kind": "ClientExtension", "selections": [ { @@ -198,7 +196,7 @@ v17 = { } ] }, -v18 = [ +v17 = [ "orderBy" ]; return { @@ -222,13 +220,9 @@ return { { "kind": "InlineFragment", "selections": [ + (v3/*: any*/), { - "args": null, - "kind": "FragmentSpread", - "name": "InviteUserDialog_currentRoleFragment" - }, - { - "args": (v5/*: any*/), + "args": (v6/*: any*/), "kind": "FragmentSpread", "name": "MemberListFragment" }, @@ -236,18 +230,20 @@ return { "kind": "RequiredField", "field": { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "members", "plural": false, - "selections": (v8/*: any*/), + "selections": [ + (v8/*: any*/) + ], "storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, "action": "THROW" }, { - "args": (v5/*: any*/), + "args": (v6/*: any*/), "kind": "FragmentSpread", "name": "InvitationListFragment" }, @@ -255,17 +251,23 @@ return { "kind": "RequiredField", "field": { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "invitations", "plural": false, - "selections": (v8/*: any*/), + "selections": [ + (v8/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "MembersPage_invitationsTotalCountFragment" + } + ], "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, "action": "THROW" - }, - (v9/*: any*/) + } ], "type": "Organization", "abstractKey": null @@ -294,32 +296,20 @@ return { "plural": false, "selections": [ (v2/*: any*/), - (v10/*: any*/), + (v9/*: any*/), { "kind": "InlineFragment", "selections": [ + (v3/*: any*/), { "alias": null, - "args": null, - "concreteType": "Membership", - "kind": "LinkedField", - "name": "viewerMembership", - "plural": false, - "selections": [ - (v11/*: any*/), - (v10/*: any*/) - ], - "storageKey": null - }, - { - "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "members", "plural": false, "selections": [ - (v7/*: any*/), + (v8/*: any*/), { "alias": null, "args": null, @@ -336,8 +326,8 @@ return { "name": "node", "plural": false, "selections": [ + (v9/*: any*/), (v10/*: any*/), - (v11/*: any*/), { "alias": null, "args": null, @@ -360,8 +350,8 @@ return { "name": "profile", "plural": false, "selections": [ - (v12/*: any*/), - (v10/*: any*/) + (v11/*: any*/), + (v9/*: any*/) ], "storageKey": null }, @@ -373,12 +363,12 @@ return { "name": "identity", "plural": false, "selections": [ - (v13/*: any*/), - (v10/*: any*/) + (v12/*: any*/), + (v9/*: any*/) ], "storageKey": null }, - (v14/*: any*/), + (v13/*: any*/), { "alias": "canUpdate", "args": [ @@ -409,19 +399,19 @@ return { ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v16/*: any*/), - (v17/*: any*/) + (v15/*: any*/), + (v16/*: any*/) ], "storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v6/*: any*/), - "filters": (v18/*: any*/), + "args": (v7/*: any*/), + "filters": (v17/*: any*/), "handle": "connection", "key": "MemberListFragment_members", "kind": "LinkedHandle", @@ -429,13 +419,12 @@ return { }, { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "invitations", "plural": false, "selections": [ - (v7/*: any*/), { "alias": null, "args": null, @@ -452,10 +441,10 @@ return { "name": "node", "plural": false, "selections": [ - (v10/*: any*/), - (v12/*: any*/), - (v13/*: any*/), + (v9/*: any*/), (v11/*: any*/), + (v12/*: any*/), + (v10/*: any*/), { "alias": null, "args": null, @@ -463,7 +452,7 @@ return { "name": "status", "storageKey": null }, - (v14/*: any*/), + (v13/*: any*/), { "alias": null, "args": null, @@ -495,25 +484,26 @@ return { ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, + (v15/*: any*/), (v16/*: any*/), - (v17/*: any*/) + (v8/*: any*/), + (v2/*: any*/) ], "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v6/*: any*/), - "filters": (v18/*: any*/), + "args": (v7/*: any*/), + "filters": (v17/*: any*/), "handle": "connection", "key": "InvitationListFragment_invitations", "kind": "LinkedHandle", "name": "invitations" - }, - (v9/*: any*/) + } ], "type": "Organization", "abstractKey": null @@ -524,16 +514,16 @@ return { ] }, "params": { - "cacheID": "9cc9ff1663db9c476c1cd67f097a4ab9", + "cacheID": "bff4e08ce3b3a6923bfb9d24fc609b18", "id": null, "metadata": {}, "name": "MembersPageQuery", "operationKind": "query", - "text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...InviteUserDialog_currentRoleFragment\n ...MemberListFragment_1jRT0c\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n ...InvitationListFragment_1jRT0c\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n canInviteUser: permission(action: \"iam:invitation:create\")\n }\n id\n }\n}\n\nfragment InvitationListFragment_1jRT0c on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment InviteUserDialog_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n" + "text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n ...MemberListFragment_1jRT0c\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n ...InvitationListFragment_1jRT0c\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n }\n id\n }\n}\n\nfragment InvitationListFragment_1jRT0c on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment InvitationListItemFragment on Invitation {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n source\n state\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership:delete\")\n}\n" } }; })(); -(node as any).hash = "591a88a5e1b0bd427daff74dc27ac512"; +(node as any).hash = "96c00bf5db547e829eaefb042658be73"; export default node; diff --git a/apps/console/src/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql.ts b/apps/console/src/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql.ts new file mode 100644 index 000000000..739f17247 --- /dev/null +++ b/apps/console/src/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql.ts @@ -0,0 +1,42 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MembersPage_invitationsTotalCountFragment$data = { + totalCount: number | null | undefined; + readonly " $fragmentType": "MembersPage_invitationsTotalCountFragment"; +}; +export type MembersPage_invitationsTotalCountFragment$key = { + readonly " $data"?: MembersPage_invitationsTotalCountFragment$data; + readonly $updatableFragmentSpreads: FragmentRefs<"MembersPage_invitationsTotalCountFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "MembersPage_invitationsTotalCountFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + } + ], + "type": "InvitationConnection", + "abstractKey": null +}; + +(node as any).hash = "af477ef9a8b791eee11ab26caee3d304"; + +export default node; diff --git a/apps/console/src/environments.ts b/apps/console/src/environments.ts index b3aff9fd4..e38788682 100644 --- a/apps/console/src/environments.ts +++ b/apps/console/src/environments.ts @@ -2,7 +2,7 @@ import { Environment, Network, RecordSource, Store } from "relay-runtime"; import { makeFetchQuery } from "@probo/relay"; export const coreEnvironment = new Environment({ - configName: "console", + configName: "core", network: Network.create(makeFetchQuery("/api/console/v1/graphql")), store: new Store(new RecordSource(), { queryCacheExpirationTime: 1 * 60 * 1000, diff --git a/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx b/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx index 2089b027d..c3eecf3a4 100644 --- a/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx +++ b/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx @@ -14,7 +14,7 @@ export const membersPageQuery = graphql` organization: node(id: $organizationId) @required(action: THROW) { __typename ... on Organization { - ...InviteUserDialog_currentRoleFragment + canInviteUser: permission(action: "iam:invitation:create") ...MemberListFragment @arguments(first: 20, order: { direction: ASC, field: CREATED_AT }) members(first: 20, orderBy: { direction: ASC, field: CREATED_AT }) @@ -26,13 +26,20 @@ export const membersPageQuery = graphql` invitations(first: 20, orderBy: { direction: ASC, field: CREATED_AT }) @required(action: THROW) { totalCount + ...MembersPage_invitationsTotalCountFragment } - canInviteUser: permission(action: "iam:invitation:create") } } } `; +export const invitationCountFragment = graphql` + fragment MembersPage_invitationsTotalCountFragment on InvitationConnection + @updatable { + totalCount + } +`; + export function MembersPage(props: { queryRef: PreloadedQuery; }) { @@ -68,7 +75,7 @@ export function MembersPage(props: { {organization.canInviteUser && ( @@ -103,6 +110,8 @@ export function MembersPage(props: { {activeTab === "invitations" && ( )} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx index bf6a339dc..00b3a268c 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx @@ -11,6 +11,7 @@ import type { InvitationListFragment$key } from "/__generated__/iam/InvitationLi import type { InvitationListFragment_RefetchQuery } from "/__generated__/iam/InvitationListFragment_RefetchQuery.graphql"; import { type ComponentProps } from "react"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import type { MembersPage_invitationsTotalCountFragment$key } from "/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql"; const fragment = graphql` fragment InvitationListFragment on Organization @@ -38,7 +39,6 @@ const fragment = graphql` ) @required(action: THROW) { __id - totalCount edges @required(action: THROW) { node { id @@ -52,8 +52,10 @@ const fragment = graphql` export function InvitationList(props: { fKey: InvitationListFragment$key; onConnectionIdChange: (connectionId: string) => void; + totalCount: number; + totalCountFKey: MembersPage_invitationsTotalCountFragment$key; }) { - const { fKey, onConnectionIdChange } = props; + const { fKey, onConnectionIdChange, totalCount, totalCountFKey } = props; const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -109,7 +111,7 @@ export function InvitationList(props: { - {invitationsPagination.data.invitations.totalCount === 0 ? ( + {totalCount === 0 ? ( {__("No invitations")} @@ -123,6 +125,7 @@ export function InvitationList(props: { key={invitation.id} fKey={invitation} onRefetch={refetchInvitations} + totalCountFKey={totalCountFKey} /> ), ) diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx index 3cbcd9bfa..2437d8227 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx @@ -15,6 +15,8 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { sprintf } from "@probo/helpers"; import { useOrganizationId } from "/hooks/useOrganizationId"; import type { InvitationListItemFragment$key } from "/__generated__/iam/InvitationListItemFragment.graphql"; +import { invitationCountFragment } from "../MembersPage"; +import type { MembersPage_invitationsTotalCountFragment$key } from "/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql"; const fragment = graphql` fragment InvitationListItemFragment on Invitation { @@ -44,9 +46,10 @@ const deleteInvitationMutation = graphql` export function InvitationListItem(props: { connectionId: string; fKey: InvitationListItemFragment$key; + totalCountFKey: MembersPage_invitationsTotalCountFragment$key; onRefetch: () => void; }) { - const { connectionId, fKey } = props; + const { connectionId, fKey, totalCountFKey } = props; const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -76,6 +79,17 @@ export function InvitationListItem(props: { }, connections: [connectionId], }, + updater: (store) => { + const { updatableData } = + store.readUpdatableFragment( + invitationCountFragment, + totalCountFKey, + ); + + if (typeof updatableData.totalCount === "number") { + updatableData.totalCount -= 1; + } + }, }); }, { diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx index 8ba739499..fe38a3801 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx @@ -11,22 +11,16 @@ import { useDialogRef, } from "@probo/ui"; import { Controller } from "react-hook-form"; -import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; import z from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import type { PropsWithChildren } from "react"; -import type { InviteUserDialog_currentRoleFragment$key } from "/__generated__/iam/InviteUserDialog_currentRoleFragment.graphql"; - -const currentRoleFragment = graphql` - fragment InviteUserDialog_currentRoleFragment on Organization { - viewerMembership @required(action: THROW) { - role - } - } -`; +import { use, type PropsWithChildren } from "react"; +import { CurrentUser } from "/providers/CurrentUser"; +import type { InviteUserDialogMutation } from "/__generated__/iam/InviteUserDialogMutation.graphql"; +import { invitationCountFragment } from "../MembersPage"; +import type { MembersPage_invitationsTotalCountFragment$key } from "/__generated__/iam/MembersPage_invitationsTotalCountFragment.graphql"; const inviteMutation = graphql` mutation InviteUserDialogMutation( @@ -50,37 +44,41 @@ const inviteMutation = graphql` } `; +// const totalCountFragment = graphql` +// fragment InviteUserDialog_totalCountFragment on InvitationConnection +// @updatable { +// totalCount +// } +// `; + const schema = z.object({ email: z.string().email(), fullName: z.string(), role: z - .enum(["OWNER", "ADMIN", "FULL", "VIEWER", "AUDITOR", "EMPLOYEE"]) + .enum(["OWNER", "ADMIN", "VIEWER", "AUDITOR", "EMPLOYEE"]) .default("VIEWER"), }); type InviteUserDialogProps = PropsWithChildren<{ connectionId: string; - viewerMembershipFKey: InviteUserDialog_currentRoleFragment$key; + fKey: MembersPage_invitationsTotalCountFragment$key; }>; export function InviteUserDialog(props: InviteUserDialogProps) { - const { children, connectionId, viewerMembershipFKey } = props; + const { children, connectionId, fKey } = props; const organizationId = useOrganizationId(); const { __ } = useTranslate(); const dialogRef = useDialogRef(); - const { viewerMembership } = - useFragment( - currentRoleFragment, - viewerMembershipFKey, - ); - const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, { - successMessage: __("Invitation sent successfully"), - errorMessage: __("Failed to send invitation"), - }); + const { role } = use(CurrentUser); + const [inviteUser, isInviting] = + useMutationWithToasts(inviteMutation, { + successMessage: __("Invitation sent successfully"), + errorMessage: __("Failed to send invitation"), + }); - const assignableRoles = getAssignableRoles(viewerMembership.role); + const assignableRoles = getAssignableRoles(role); const { register, handleSubmit, formState, reset, control } = useFormWithSchema(schema, { @@ -102,6 +100,17 @@ export function InviteUserDialog(props: InviteUserDialogProps) { reset(); dialogRef.current?.close(); }, + updater: (store) => { + const { updatableData } = + store.readUpdatableFragment( + invitationCountFragment, + fKey, + ); + + if (typeof updatableData.totalCount === "number") { + updatableData.totalCount += 1; + } + }, }); }); diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 3cc263375..3a9828629 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -1225,10 +1225,13 @@ func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organ return nil, nil } + filters := coredata.NewInvitationFilter(nil) + if gqlutils.OnlyTotalCountSelected(ctx) { return &types.InvitationConnection{ Resolver: r, ParentID: obj.ID, + Filters: filters, }, nil } @@ -1243,7 +1246,6 @@ func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organ } } - filters := coredata.NewInvitationFilter(nil) if status != nil { filters = coredata.NewInvitationFilter([]coredata.InvitationStatus{*status}) }