From 278411392d5c7c5f2dc2649810412a1b627f4643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Sat, 27 Dec 2025 00:37:15 +0100 Subject: [PATCH] Replug invitations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../organizations/settings/MembersPage.tsx | 101 ++--- .../__generated__/MembersPageQuery.graphql.ts | 408 ++++++++++++------ .../settings/_components/InvitationList.tsx | 117 +++++ .../_components/InvitationListItem.tsx | 151 +++++++ .../settings/_components/InviteUserDialog.tsx | 227 ++++++++++ .../settings/_components/MemberList.tsx | 13 +- .../settings/_components/MemberListItem.tsx | 18 +- .../InvitationListFragment.graphql.ts | 244 +++++++++++ ...tationListFragment_RefetchQuery.graphql.ts | 375 ++++++++++++++++ .../InvitationListItemFragment.graphql.ts | 100 +++++ ...stItem_DeleteInvitationMutation.graphql.ts | 133 ++++++ ...ionListItem_permissionsFragment.graphql.ts | 59 +++ .../InviteUserDialogMutation.graphql.ts | 211 +++++++++ ...eUserDialog_currentRoleFragment.graphql.ts | 60 +++ .../MemberListFragment.graphql.ts | 6 +- ...MemberListFragment_RefetchQuery.graphql.ts | 6 +- pkg/server/api/connect/v1/schema.graphql | 4 + pkg/server/api/connect/v1/schema/schema.go | 76 +++- pkg/server/api/connect/v1/types/invitation.go | 1 + pkg/server/api/connect/v1/types/types.go | 9 +- pkg/server/api/connect/v1/v1_resolver.go | 2 +- 21 files changed, 2084 insertions(+), 237 deletions(-) create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment_RefetchQuery.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItemFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_DeleteInvitationMutation.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_permissionsFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialogMutation.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialog_currentRoleFragment.graphql.ts diff --git a/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx b/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx index cfa12ede9..cc6efde04 100644 --- a/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx +++ b/apps/console/src/pages/iam/organizations/settings/MembersPage.tsx @@ -3,24 +3,39 @@ import { graphql } from "relay-runtime"; import { MemberList } from "./_components/MemberList"; import type { MembersPageQuery } from "./__generated__/MembersPageQuery.graphql"; import { useTranslate } from "@probo/i18n"; -import { Card, TabBadge, TabItem, Tabs } from "@probo/ui"; +import { Button, Card, TabBadge, TabItem, Tabs } from "@probo/ui"; import { useState } from "react"; +import { InvitationList } from "./_components/InvitationList"; +import { InviteUserDialog } from "./_components/InviteUserDialog"; export const membersPageQuery = graphql` query MembersPageQuery($organizationId: ID!) { viewer @required(action: THROW) { + canInviteUser: permission( + action: "iam:membership:create" + id: $organizationId + ) + ...InvitationListItem_permissionsFragment + @arguments(organizationId: $organizationId) ...MemberListItem_permissionsFragment @arguments(organizationId: $organizationId) } organization: node(id: $organizationId) @required(action: THROW) { __typename ... on Organization { + ...InviteUserDialog_currentRoleFragment ...MemberListFragment @arguments(first: 20, order: { direction: ASC, field: CREATED_AT }) members(first: 20, orderBy: { direction: ASC, field: CREATED_AT }) @required(action: THROW) { totalCount } + ...InvitationListFragment + @arguments(first: 20, order: { direction: ASC, field: CREATED_AT }) + invitations(first: 20, orderBy: { direction: ASC, field: CREATED_AT }) + @required(action: THROW) { + totalCount + } } } } @@ -49,14 +64,11 @@ export function MembersPage(props: {

{__("Workspace members")}

- {/* {isAuthorized("Organization", "inviteUser") && ( - + {viewer.canInviteUser && ( + - )} */} + )}
@@ -69,85 +81,26 @@ export function MembersPage(props: { {organization.members.totalCount} )} - {/* setActiveTab("invitations")} > {__("Invitations")} - {(invitationsPagination.data.invitations?.totalCount || 0) > 0 && ( - - {invitationsPagination.data.invitations?.totalCount} - + {(organization.invitations.totalCount ?? 0) > 0 && ( + {organization.invitations.totalCount} )} - */} +
{activeTab === "memberships" && ( - + )} - {/* {activeTab === "invitations" && ( - { - invitationsPagination.refetch({ - order: { - direction: order.direction as "ASC" | "DESC", - field: order.field as - | "CREATED_AT" - | "EXPIRES_AT" - | "FULL_NAME" - | "EMAIL" - | "ROLE" - | "STATUS" - | "ACCEPTED_AT", - }, - }); - }} - pageSize={20} - > - - - {__("Name")} - {__("Email")} - {__("Role")} - {__("Invited")} - {__("Status")} - - {__("Accepted at")} - - - - - - {invitations.length === 0 ? ( - - - {__("No invitations")} - - - ) : ( - invitations.map((invitation) => ( - - )) - )} - - - )} */} + {activeTab === "invitations" && ( + + )}
diff --git a/apps/console/src/pages/iam/organizations/settings/__generated__/MembersPageQuery.graphql.ts b/apps/console/src/pages/iam/organizations/settings/__generated__/MembersPageQuery.graphql.ts index 97ad651a1..b3e2f35ea 100644 --- a/apps/console/src/pages/iam/organizations/settings/__generated__/MembersPageQuery.graphql.ts +++ b/apps/console/src/pages/iam/organizations/settings/__generated__/MembersPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<33faed6358cc7fba56a1f493c11f31d4>> * @lightSyntaxTransform * @nogrep */ @@ -16,17 +16,21 @@ export type MembersPageQuery$variables = { export type MembersPageQuery$data = { readonly organization: { readonly __typename: "Organization"; + readonly invitations: { + readonly totalCount: number | null | undefined; + }; readonly members: { readonly totalCount: number | null | undefined; }; - readonly " $fragmentSpreads": FragmentRefs<"MemberListFragment">; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment" | "InviteUserDialog_currentRoleFragment" | "MemberListFragment">; } | { // This will never be '%other', but we need some // value in case none of the concrete values match. readonly __typename: "%other"; }; readonly viewer: { - readonly " $fragmentSpreads": FragmentRefs<"MemberListItem_permissionsFragment">; + readonly canInviteUser: boolean; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListItem_permissionsFragment" | "MemberListItem_permissionsFragment">; }; }; export type MembersPageQuery = { @@ -47,54 +51,168 @@ v1 = { "name": "id", "variableName": "organizationId" }, -v2 = [ +v2 = { + "alias": "canInviteUser", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership:create" + }, + (v1/*: any*/) + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": null +}, +v3 = [ + { + "kind": "Variable", + "name": "organizationId", + "variableName": "organizationId" + } +], +v4 = [ (v1/*: any*/) ], -v3 = { +v5 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v4 = { +v6 = { "kind": "Literal", "name": "first", "value": 20 }, -v5 = { +v7 = { "direction": "ASC", "field": "CREATED_AT" }, -v6 = [ - (v4/*: any*/), +v8 = [ + (v6/*: any*/), + { + "kind": "Literal", + "name": "order", + "value": (v7/*: any*/) + } +], +v9 = [ + (v6/*: any*/), { "kind": "Literal", "name": "orderBy", - "value": (v5/*: any*/) + "value": (v7/*: any*/) } ], -v7 = { +v10 = { "alias": null, "args": null, "kind": "ScalarField", "name": "totalCount", "storageKey": null }, -v8 = { +v11 = [ + (v10/*: any*/) +], +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v9 = { +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "role", "storageKey": null -}; +}, +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v15 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null +}, +v16 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v17 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v18 = { + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null +}, +v19 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v20 = [ + "orderBy" +]; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -112,14 +230,14 @@ return { "name": "viewer", "plural": false, "selections": [ + (v2/*: any*/), { - "args": [ - { - "kind": "Variable", - "name": "organizationId", - "variableName": "organizationId" - } - ], + "args": (v3/*: any*/), + "kind": "FragmentSpread", + "name": "InvitationListItem_permissionsFragment" + }, + { + "args": (v3/*: any*/), "kind": "FragmentSpread", "name": "MemberListItem_permissionsFragment" } @@ -132,25 +250,23 @@ return { "kind": "RequiredField", "field": { "alias": "organization", - "args": (v2/*: any*/), + "args": (v4/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - (v3/*: any*/), + (v5/*: any*/), { "kind": "InlineFragment", "selections": [ { - "args": [ - (v4/*: any*/), - { - "kind": "Literal", - "name": "order", - "value": (v5/*: any*/) - } - ], + "args": null, + "kind": "FragmentSpread", + "name": "InviteUserDialog_currentRoleFragment" + }, + { + "args": (v8/*: any*/), "kind": "FragmentSpread", "name": "MemberListFragment" }, @@ -158,17 +274,34 @@ return { "kind": "RequiredField", "field": { "alias": null, - "args": (v6/*: any*/), + "args": (v9/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "members", "plural": false, - "selections": [ - (v7/*: any*/) - ], + "selections": (v11/*: any*/), "storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, "action": "THROW" + }, + { + "args": (v8/*: any*/), + "kind": "FragmentSpread", + "name": "InvitationListFragment" + }, + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": (v9/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": (v11/*: any*/), + "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + }, + "action": "THROW" } ], "type": "Organization", @@ -197,6 +330,21 @@ return { "name": "viewer", "plural": false, "selections": [ + (v2/*: any*/), + { + "alias": "canDeleteInvitation", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:invitation:delete" + }, + (v1/*: any*/) + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": null + }, { "alias": "canUpdateMembership", "args": [ @@ -225,20 +373,20 @@ return { "name": "permission", "storageKey": null }, - (v8/*: any*/) + (v12/*: any*/) ], "storageKey": null }, { "alias": "organization", - "args": (v2/*: any*/), + "args": (v4/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - (v3/*: any*/), - (v8/*: any*/), + (v5/*: any*/), + (v12/*: any*/), { "kind": "InlineFragment", "selections": [ @@ -250,20 +398,20 @@ return { "name": "viewerMembership", "plural": false, "selections": [ - (v9/*: any*/), - (v8/*: any*/) + (v13/*: any*/), + (v12/*: any*/) ], "storageKey": null }, { "alias": null, - "args": (v6/*: any*/), + "args": (v9/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "members", "plural": false, "selections": [ - (v7/*: any*/), + (v10/*: any*/), { "alias": null, "args": null, @@ -280,8 +428,8 @@ return { "name": "node", "plural": false, "selections": [ - (v8/*: any*/), - (v9/*: any*/), + (v12/*: any*/), + (v13/*: any*/), { "alias": null, "args": null, @@ -290,14 +438,8 @@ return { "name": "profile", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - }, - (v8/*: any*/) + (v14/*: any*/), + (v12/*: any*/) ], "storageKey": null }, @@ -309,102 +451,106 @@ return { "name": "identity", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "email", - "storageKey": null - }, - (v8/*: any*/) + (v15/*: any*/), + (v12/*: any*/) ], "storageKey": null }, + (v16/*: any*/), + (v5/*: any*/) + ], + "storageKey": null + }, + (v17/*: any*/) + ], + "storageKey": null + }, + (v18/*: any*/), + (v19/*: any*/) + ], + "storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + }, + { + "alias": null, + "args": (v9/*: any*/), + "filters": (v20/*: any*/), + "handle": "connection", + "key": "MemberListFragment_members", + "kind": "LinkedHandle", + "name": "members" + }, + { + "alias": null, + "args": (v9/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": [ + (v10/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + (v14/*: any*/), + (v15/*: any*/), + (v13/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + (v16/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "createdAt", + "name": "acceptedAt", "storageKey": null }, - (v3/*: any*/) + (v5/*: any*/) ], "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } + (v17/*: any*/) ], "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 - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasPreviousPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "startCursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] - } + (v18/*: any*/), + (v19/*: any*/) ], - "storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + "storageKey": "invitations(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v6/*: any*/), - "filters": [ - "orderBy" - ], + "args": (v9/*: any*/), + "filters": (v20/*: any*/), "handle": "connection", - "key": "MembersListFragment_members", + "key": "InvitationListFragment_invitations", "kind": "LinkedHandle", - "name": "members" + "name": "invitations" } ], "type": "Organization", @@ -416,16 +562,16 @@ return { ] }, "params": { - "cacheID": "b125343184bf4d82a7d0e264faf7ac34", + "cacheID": "1c006f49a510a658f66a78c66e9289db", "id": null, "metadata": {}, "name": "MembersPageQuery", "operationKind": "query", - "text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n viewer {\n ...MemberListItem_permissionsFragment_4xMPKw\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MemberListFragment_1jRT0c\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n }\n id\n }\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n ...MemberListItem_currentRoleFragment\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 profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n}\n\nfragment MemberListItem_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListItem_permissionsFragment_4xMPKw on Identity {\n canUpdateMembership: permission(action: \"iam:membership:update\", id: $organizationId)\n canDeleteMembership: permission(action: \"iam:membership:delete\", id: $organizationId)\n}\n" + "text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n viewer {\n canInviteUser: permission(action: \"iam:membership:create\", id: $organizationId)\n ...InvitationListItem_permissionsFragment_4xMPKw\n ...MemberListItem_permissionsFragment_4xMPKw\n 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 }\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}\n\nfragment InvitationListItem_permissionsFragment_4xMPKw on Identity {\n canDeleteInvitation: permission(action: \"iam:invitation:delete\", id: $organizationId)\n}\n\nfragment InviteUserDialog_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n ...MemberListItem_currentRoleFragment\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 profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n}\n\nfragment MemberListItem_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListItem_permissionsFragment_4xMPKw on Identity {\n canUpdateMembership: permission(action: \"iam:membership:update\", id: $organizationId)\n canDeleteMembership: permission(action: \"iam:membership:delete\", id: $organizationId)\n}\n" } }; })(); -(node as any).hash = "8c40640ce96580ebae677d117741094e"; +(node as any).hash = "6e919357c168bbfbdccfa7375ce71660"; export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx new file mode 100644 index 000000000..5da504d58 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/InvitationList.tsx @@ -0,0 +1,117 @@ +import { Tbody, Td, Th, Thead, Tr } from "@probo/ui"; +import { SortableTable, SortableTh } from "/components/SortableTable"; +import { useTranslate } from "@probo/i18n"; +import { graphql, usePaginationFragment } from "react-relay"; +import { InvitationListItem } from "./InvitationListItem"; +import type { InvitationListFragment$key } from "./__generated__/InvitationListFragment.graphql"; +import type { InvitationListItem_permissionsFragment$key } from "./__generated__/InvitationListItem_permissionsFragment.graphql"; +import type { InvitationListFragment_RefetchQuery } from "./__generated__/InvitationListFragment_RefetchQuery.graphql"; + +const fragment = graphql` + fragment InvitationListFragment on Organization + @refetchable(queryName: "InvitationListFragment_RefetchQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + order: { + type: "InvitationOrder" + defaultValue: { direction: ASC, field: CREATED_AT } + } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + invitations( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) + @connection(key: "InvitationListFragment_invitations") + @required(action: THROW) { + __id + totalCount + edges @required(action: THROW) { + node { + id + ...InvitationListItemFragment + } + } + } + } +`; + +export function InvitationList(props: { + fKey: InvitationListFragment$key; + permissionsFKey: InvitationListItem_permissionsFragment$key; +}) { + const { fKey, permissionsFKey } = props; + + const { __ } = useTranslate(); + + const invitationsPagination = usePaginationFragment< + InvitationListFragment_RefetchQuery, + InvitationListFragment$key + >(fragment, fKey); + + const refetchInvitations = () => { + invitationsPagination.refetch({}, { fetchPolicy: "network-only" }); + }; + + return ( + { + invitationsPagination.refetch({ + order: { + direction: order.direction as "ASC" | "DESC", + field: order.field as + | "CREATED_AT" + | "EXPIRES_AT" + // FIXME: put back + // | "FULL_NAME" + | "EMAIL" + | "ROLE" + // FIXME: put back + // | "STATUS" + | "ACCEPTED_AT", + }, + }); + }} + pageSize={20} + > + + + {__("Name")} + {__("Email")} + {__("Role")} + {__("Invited")} + {__("Status")} + {__("Accepted at")} + + + + + {invitationsPagination.data.invitations.totalCount === 0 ? ( + + + {__("No invitations")} + + + ) : ( + invitationsPagination.data.invitations.edges.map( + ({ node: invitation }) => ( + + ), + ) + )} + + + ); +} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx new file mode 100644 index 000000000..c5e73fe28 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/InvitationListItem.tsx @@ -0,0 +1,151 @@ +import { useTranslate } from "@probo/i18n"; +import { + Badge, + Button, + IconTrashCan, + Spinner, + Td, + Tr, + useConfirm, +} from "@probo/ui"; +import clsx from "clsx"; +import { useFragment } from "react-relay"; +import { graphql } from "relay-runtime"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { sprintf } from "@probo/helpers"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import type { InvitationListItemFragment$key } from "./__generated__/InvitationListItemFragment.graphql"; +import type { InvitationListItem_permissionsFragment$key } from "./__generated__/InvitationListItem_permissionsFragment.graphql"; + +const fragment = graphql` + fragment InvitationListItemFragment on Invitation { + id + fullName + email + role + status + createdAt + expiresAt + acceptedAt + } +`; + +const permissionsFragment = graphql` + fragment InvitationListItem_permissionsFragment on Identity + @argumentDefinitions(organizationId: { type: "ID!" }) { + canDeleteInvitation: permission( + action: "iam:invitation:delete" + id: $organizationId + ) + } +`; + +const deleteInvitationMutation = graphql` + mutation InvitationListItem_DeleteInvitationMutation( + $input: DeleteInvitationInput! + $connections: [ID!]! + ) { + deleteInvitation(input: $input) { + deletedInvitationId @deleteEdge(connections: $connections) + } + } +`; + +export function InvitationListItem(props: { + connectionId: string; + fKey: InvitationListItemFragment$key; + permissionsFKey: InvitationListItem_permissionsFragment$key; + onRefetch: () => void; +}) { + const { connectionId, fKey, permissionsFKey } = props; + + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + const confirm = useConfirm(); + + const invitation = useFragment( + fragment, + fKey, + ); + const permissions = useFragment( + permissionsFragment, + permissionsFKey, + ); + + const [deleteInvitation, isDeleting] = useMutationWithToasts( + deleteInvitationMutation, + { + successMessage: __("Invitation deleted successfully"), + errorMessage: __("Failed to delete invitation"), + }, + ); + + const handleDelete = () => { + confirm( + () => { + return deleteInvitation({ + variables: { + input: { + organizationId, + invitationId: invitation.id, + }, + connections: [connectionId], + }, + }); + }, + { + message: sprintf( + __("Are you sure you want to delete the invitation for %s?"), + invitation.fullName, + ), + }, + ); + }; + + return ( + + +
{invitation.fullName}
+ + {invitation.email} + + {invitation.role} + + {new Date(invitation.createdAt).toLocaleDateString()} + + {invitation.status === "ACCEPTED" ? ( + {__("Accepted")} + ) : invitation.status === "EXPIRED" ? ( + {__("Expired")} + ) : ( + {__("Pending")} + )} + + + {invitation.acceptedAt + ? new Date(invitation.acceptedAt).toLocaleDateString() + : "-"} + + +
e.stopPropagation()} + > + {isDeleting ? ( + + ) : ( + permissions.canDeleteInvitation && ( +
+ + + ); +} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx new file mode 100644 index 000000000..c39a094d9 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx @@ -0,0 +1,227 @@ +import { getAssignableRoles } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { + Dialog, + DialogContent, + Field, + Select, + Option, + DialogFooter, + Button, + Checkbox, + useDialogRef, +} from "@probo/ui"; +import { Controller } from "react-hook-form"; +import { useFragment } from "react-relay"; +import { ConnectionHandler, 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__/InviteUserDialog_currentRoleFragment.graphql"; + +const currentRoleFragment = graphql` + fragment InviteUserDialog_currentRoleFragment on Organization { + viewerMembership @required(action: THROW) { + role + } + } +`; + +const inviteMutation = graphql` + mutation InviteUserDialogMutation( + $input: InviteMemberInput! + $connections: [ID!]! + ) { + inviteMember(input: $input) { + invitationEdge @prependEdge(connections: $connections) { + node { + id + email + fullName + role + expiresAt + acceptedAt + createdAt + } + } + } + } +`; + +const schema = z.object({ + email: z.string().email(), + fullName: z.string(), + role: z + .enum(["OWNER", "ADMIN", "FULL", "VIEWER", "AUDITOR", "EMPLOYEE"]) + .default("VIEWER"), + createPeople: z.boolean().default(false), +}); + +type InviteUserDialogProps = PropsWithChildren<{ + viewerMembershipFKey: InviteUserDialog_currentRoleFragment$key; +}>; + +export function InviteUserDialog(props: InviteUserDialogProps) { + const { children, viewerMembershipFKey } = 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 assignableRoles = getAssignableRoles(viewerMembership.role); + + const { register, handleSubmit, formState, reset, control } = + useFormWithSchema(schema, { + defaultValues: { role: "VIEWER", createPeople: false }, + }); + + const onSubmit = handleSubmit((data) => { + const connectionId = ConnectionHandler.getConnectionID( + organizationId, + "InvitationListFragment_invitations", + ); + inviteUser({ + variables: { + input: { + organizationId, + email: data.email, + fullName: data.fullName, + role: data.role, + createPeople: data.createPeople, + }, + connections: [connectionId], + }, + onCompleted: () => { + reset(); + dialogRef.current?.close(); + }, + }); + }); + + return ( + +
+ +

+ Send an invitation to join your workspace. +

+ + + + ( + <> + +
+ {field.value === "OWNER" && ( +

{__("Full access to everything")}

+ )} + {field.value === "ADMIN" && ( +

+ {__( + "Full access except organization setup and API keys", + )} +

+ )} + {field.value === "VIEWER" && ( +

{__("Read-only access")}

+ )} + {field.value === "AUDITOR" && ( +

+ {__( + "Read-only access without settings, tasks and meetings", + )} +

+ )} + {field.value === "EMPLOYEE" && ( +

{__("Access to employee page")}

+ )} +
+ + )} + /> +
+
+
+ ( + <> + + + + )} + /> +
+

+ {__( + "Creates a people record for this user in addition to the user account", + )} +

+
+
+ + + +
+
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx b/apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx index 7397c58e4..720f9630d 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/MemberList.tsx @@ -27,7 +27,7 @@ const fragment = graphql` last: $last before: $before orderBy: $order - ) @connection(key: "MembersListFragment_members") @required(action: THROW) { + ) @connection(key: "MemberListFragment_members") @required(action: THROW) { __id totalCount edges @required(action: THROW) { @@ -42,9 +42,9 @@ const fragment = graphql` export function MemberList(props: { fKey: MemberListFragment$key; - viewerFKey: MemberListItem_permissionsFragment$key; + permissionsFKey: MemberListItem_permissionsFragment$key; }) { - const { fKey, viewerFKey } = props; + const { fKey, permissionsFKey } = props; const { __ } = useTranslate(); @@ -85,7 +85,7 @@ export function MemberList(props: { - {membersPagination.data.members.edges.length === 0 ? ( + {membersPagination.data.members.totalCount === 0 ? ( {__("No members")} @@ -94,11 +94,12 @@ export function MemberList(props: { ) : ( membersPagination.data.members.edges.map(({ node: membership }) => ( )) )} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx b/apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx index 590ddffea..53cbbc05f 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/MemberListItem.tsx @@ -12,7 +12,7 @@ import { import clsx from "clsx"; import { useState } from "react"; import { useFragment } from "react-relay"; -import { ConnectionHandler, graphql } from "relay-runtime"; +import { graphql } from "relay-runtime"; import type { MemberListItemFragment$key } from "./__generated__/MemberListItemFragment.graphql"; import type { MemberListItem_permissionsFragment$key } from "./__generated__/MemberListItem_permissionsFragment.graphql"; import type { MemberListItem_currentRoleFragment$key } from "./__generated__/MemberListItem_currentRoleFragment.graphql"; @@ -69,12 +69,13 @@ const removeMemberMutation = graphql` `; export function MemberListItem(props: { + connectionId: string; fKey: MemberListItemFragment$key; - permissionsFKey: MemberListItem_currentRoleFragment$key; - viewerFKey: MemberListItem_permissionsFragment$key; + permissionsFKey: MemberListItem_permissionsFragment$key; + viewerFKey: MemberListItem_currentRoleFragment$key; onRefetch: () => void; }) { - const { fKey, onRefetch, permissionsFKey, viewerFKey } = props; + const { fKey, connectionId, permissionsFKey, viewerFKey } = props; const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -85,11 +86,11 @@ export function MemberListItem(props: { const { viewerMembership } = useFragment( currentRoleFragment, - permissionsFKey, + viewerFKey, ); const permissions = useFragment( permissionsFragment, - viewerFKey, + permissionsFKey, ); // Only OWNER can edit OWNER members @@ -105,10 +106,6 @@ export function MemberListItem(props: { ); const handleRemove = async () => { - const connectionId = ConnectionHandler.getConnectionID( - organizationId, - "MembersListFragment_members", - ); confirm( () => { return removeMembership({ @@ -119,7 +116,6 @@ export function MemberListItem(props: { }, connections: [connectionId], }, - onCompleted: onRefetch, }); }, { diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment.graphql.ts new file mode 100644 index 000000000..3c94eb619 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment.graphql.ts @@ -0,0 +1,244 @@ +/** + * @generated SignedSource<<6067ce11fa959cba895d2b10862592de>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type InvitationListFragment$data = { + readonly id: string; + readonly invitations: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListItemFragment">; + }; + }>; + readonly totalCount: number | null | undefined; + }; + readonly " $fragmentType": "InvitationListFragment"; +}; +export type InvitationListFragment$key = { + readonly " $data"?: InvitationListFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment">; +}; + +import InvitationListFragment_RefetchQuery_graphql from './InvitationListFragment_RefetchQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "invitations" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": { + "direction": "ASC", + "field": "CREATED_AT" + }, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": InvitationListFragment_RefetchQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "InvitationListFragment", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": "invitations", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "__InvitationListFragment_invitations_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + }, + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "InvitationListItemFragment" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + }, + { + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + "action": "THROW" + }, + (v1/*: any*/) + ], + "type": "Organization", + "abstractKey": null +}; +})(); + +(node as any).hash = "837e567be489574a7f0350e26a8e5454"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment_RefetchQuery.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment_RefetchQuery.graphql.ts new file mode 100644 index 000000000..dc232f7a4 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListFragment_RefetchQuery.graphql.ts @@ -0,0 +1,375 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type InvitationOrderField = "ACCEPTED_AT" | "CREATED_AT" | "EMAIL" | "EXPIRES_AT" | "ROLE"; +export type OrderDirection = "ASC" | "DESC"; +export type InvitationOrder = { + direction: OrderDirection; + field: InvitationOrderField; +}; +export type InvitationListFragment_RefetchQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: InvitationOrder | null | undefined; +}; +export type InvitationListFragment_RefetchQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"InvitationListFragment">; + } | null | undefined; +}; +export type InvitationListFragment_RefetchQuery = { + response: InvitationListFragment_RefetchQuery$data; + variables: InvitationListFragment_RefetchQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": { + "direction": "ASC", + "field": "CREATED_AT" + }, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "InvitationListFragment_RefetchQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "InvitationListFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "InvitationListFragment_RefetchQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "invitations", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + (v11/*: 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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "InvitationListFragment_invitations", + "kind": "LinkedHandle", + "name": "invitations" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "4cc36c8fb50b104ebe3fae66bf847ecc", + "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}\n" + } +}; +})(); + +(node as any).hash = "837e567be489574a7f0350e26a8e5454"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItemFragment.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItemFragment.graphql.ts new file mode 100644 index 000000000..ac189f769 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItemFragment.graphql.ts @@ -0,0 +1,100 @@ +/** + * @generated SignedSource<<748bbadd485b1e131e583c119f980e91>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING"; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; +import { FragmentRefs } from "relay-runtime"; +export type InvitationListItemFragment$data = { + readonly acceptedAt: any | null | undefined; + readonly createdAt: any; + readonly email: any; + readonly expiresAt: any; + readonly fullName: string; + readonly id: string; + readonly role: MembershipRole; + readonly status: InvitationStatus; + readonly " $fragmentType": "InvitationListItemFragment"; +}; +export type InvitationListItemFragment$key = { + readonly " $data"?: InvitationListItemFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListItemFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "InvitationListItemFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + } + ], + "type": "Invitation", + "abstractKey": null +}; + +(node as any).hash = "772414270b8ce2fed6c7b5fe97082c17"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_DeleteInvitationMutation.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_DeleteInvitationMutation.graphql.ts new file mode 100644 index 000000000..e86a9f30d --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_DeleteInvitationMutation.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteInvitationInput = { + invitationId: string; + organizationId: string; +}; +export type InvitationListItem_DeleteInvitationMutation$variables = { + connections: ReadonlyArray; + input: DeleteInvitationInput; +}; +export type InvitationListItem_DeleteInvitationMutation$data = { + readonly deleteInvitation: { + readonly deletedInvitationId: string; + } | null | undefined; +}; +export type InvitationListItem_DeleteInvitationMutation = { + response: InvitationListItem_DeleteInvitationMutation$data; + variables: InvitationListItem_DeleteInvitationMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedInvitationId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "InvitationListItem_DeleteInvitationMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteInvitationPayload", + "kind": "LinkedField", + "name": "deleteInvitation", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "InvitationListItem_DeleteInvitationMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteInvitationPayload", + "kind": "LinkedField", + "name": "deleteInvitation", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedInvitationId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "35736b4156c15bb9eeb863b4d40fc4e6", + "id": null, + "metadata": {}, + "name": "InvitationListItem_DeleteInvitationMutation", + "operationKind": "mutation", + "text": "mutation InvitationListItem_DeleteInvitationMutation(\n $input: DeleteInvitationInput!\n) {\n deleteInvitation(input: $input) {\n deletedInvitationId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "2191c08f6c6bf5bace0d70a6481124bb"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_permissionsFragment.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_permissionsFragment.graphql.ts new file mode 100644 index 000000000..86a6023ce --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InvitationListItem_permissionsFragment.graphql.ts @@ -0,0 +1,59 @@ +/** + * @generated SignedSource<<0fa501d76c400538c62a44425ebe4b11>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type InvitationListItem_permissionsFragment$data = { + readonly canDeleteInvitation: boolean; + readonly " $fragmentType": "InvitationListItem_permissionsFragment"; +}; +export type InvitationListItem_permissionsFragment$key = { + readonly " $data"?: InvitationListItem_permissionsFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"InvitationListItem_permissionsFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } + ], + "kind": "Fragment", + "metadata": null, + "name": "InvitationListItem_permissionsFragment", + "selections": [ + { + "alias": "canDeleteInvitation", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:invitation:delete" + }, + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": null + } + ], + "type": "Identity", + "abstractKey": null +}; + +(node as any).hash = "e9e75ae0f5a3755a777acf16e5024ee0"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialogMutation.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialogMutation.graphql.ts new file mode 100644 index 000000000..f4ca2f757 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialogMutation.graphql.ts @@ -0,0 +1,211 @@ +/** + * @generated SignedSource<<84f3f3da89ad2bdfecd79b479bf74814>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; +export type InviteMemberInput = { + createPeople: boolean; + email: any; + fullName: string; + organizationId: string; + role: MembershipRole; +}; +export type InviteUserDialogMutation$variables = { + connections: ReadonlyArray; + input: InviteMemberInput; +}; +export type InviteUserDialogMutation$data = { + readonly inviteMember: { + readonly invitationEdge: { + readonly node: { + readonly acceptedAt: any | null | undefined; + readonly createdAt: any; + readonly email: any; + readonly expiresAt: any; + readonly fullName: string; + readonly id: string; + readonly role: MembershipRole; + }; + }; + } | null | undefined; +}; +export type InviteUserDialogMutation = { + response: InviteUserDialogMutation$data; + variables: InviteUserDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "concreteType": "InvitationEdge", + "kind": "LinkedField", + "name": "invitationEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "acceptedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "InviteUserDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "InviteMemberPayload", + "kind": "LinkedField", + "name": "inviteMember", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "InviteUserDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "InviteMemberPayload", + "kind": "LinkedField", + "name": "inviteMember", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "invitationEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "43ea730d742b7d259b50b50d3684147c", + "id": null, + "metadata": {}, + "name": "InviteUserDialogMutation", + "operationKind": "mutation", + "text": "mutation InviteUserDialogMutation(\n $input: InviteMemberInput!\n) {\n inviteMember(input: $input) {\n invitationEdge {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "b45737075b750b79b3a90748cb2565f3"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialog_currentRoleFragment.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialog_currentRoleFragment.graphql.ts new file mode 100644 index 000000000..b9981c585 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/InviteUserDialog_currentRoleFragment.graphql.ts @@ -0,0 +1,60 @@ +/** + * @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/pages/iam/organizations/settings/_components/__generated__/MemberListFragment.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment.graphql.ts index e45d14616..511ba41cd 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment.graphql.ts +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<30bea11238a554ed69a88c9f07d33d9e>> * @lightSyntaxTransform * @nogrep */ @@ -126,7 +126,7 @@ return { ], "concreteType": "MembershipConnection", "kind": "LinkedField", - "name": "__MembersListFragment_members_connection", + "name": "__MemberListFragment_members_connection", "plural": false, "selections": [ { @@ -245,6 +245,6 @@ return { }; })(); -(node as any).hash = "dcdc5a8acd5e8f7317fbd1bcd3570f61"; +(node as any).hash = "909f6c9ed0fcaf7b0d170bded8c97562"; export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment_RefetchQuery.graphql.ts b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment_RefetchQuery.graphql.ts index 05978133b..3999f1eab 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment_RefetchQuery.graphql.ts +++ b/apps/console/src/pages/iam/organizations/settings/_components/__generated__/MemberListFragment_RefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<4966da1a2a597f71a90c8766ad760615>> + * @generated SignedSource<<75b9df3cc430ed26d41b88a67a410aca>> * @lightSyntaxTransform * @nogrep */ @@ -363,7 +363,7 @@ return { "orderBy" ], "handle": "connection", - "key": "MembersListFragment_members", + "key": "MemberListFragment_members", "kind": "LinkedHandle", "name": "members" } @@ -387,6 +387,6 @@ return { }; })(); -(node as any).hash = "dcdc5a8acd5e8f7317fbd1bcd3570f61"; +(node as any).hash = "909f6c9ed0fcaf7b0d170bded8c97562"; export default node; diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index e6b6283fc..99c019f60 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -206,6 +206,7 @@ type Organization implements Node { last: Int before: CursorKey status: InvitationStatus + orderBy: InvitationOrder ): InvitationConnection @goField(forceResolver: true) samlConfigurations( @@ -244,6 +245,7 @@ type Membership implements Node { type Invitation implements Node { id: ID! email: EmailAddr! + fullName: String! role: MembershipRole! expiresAt: Datetime! acceptedAt: Datetime @@ -600,6 +602,8 @@ input InviteMemberInput { organizationId: ID! email: EmailAddr! fullName: String! + role: MembershipRole! + createPeople: Boolean! } input UpdateMembershipInput { diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 55298f222..1493823ce 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -147,6 +147,7 @@ type ComplexityRoot struct { CreatedAt func(childComplexity int) int Email func(childComplexity int) int ExpiresAt func(childComplexity int) int + FullName func(childComplexity int) int ID func(childComplexity int) int Organization func(childComplexity int) int Role func(childComplexity int) int @@ -234,7 +235,7 @@ type ComplexityRoot struct { HeadquarterAddress func(childComplexity int) int HorizontalLogoURL func(childComplexity int) int ID func(childComplexity int) int - Invitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) int + Invitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus, orderBy *types.InvitationOrderBy) int LogoURL func(childComplexity int) int Members func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) int Name func(childComplexity int) int @@ -479,7 +480,7 @@ type OrganizationResolver interface { HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) Members(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) - Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) (*types.InvitationConnection, error) + Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus, orderBy *types.InvitationOrderBy) (*types.InvitationConnection, error) SamlConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SAMLConfigurationConnection, error) ViewerMembership(ctx context.Context, obj *types.Organization) (*types.Membership, error) } @@ -767,6 +768,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Invitation.ExpiresAt(childComplexity), true + case "Invitation.fullName": + if e.complexity.Invitation.FullName == nil { + break + } + + return e.complexity.Invitation.FullName(childComplexity), true case "Invitation.id": if e.complexity.Invitation.ID == nil { break @@ -1271,7 +1278,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Organization.Invitations(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["status"].(*coredata.InvitationStatus)), true + return e.complexity.Organization.Invitations(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["status"].(*coredata.InvitationStatus), args["orderBy"].(*types.InvitationOrderBy)), true case "Organization.logoUrl": if e.complexity.Organization.LogoURL == nil { break @@ -2206,6 +2213,7 @@ type Organization implements Node { last: Int before: CursorKey status: InvitationStatus + orderBy: InvitationOrder ): InvitationConnection @goField(forceResolver: true) samlConfigurations( @@ -2244,6 +2252,7 @@ type Membership implements Node { type Invitation implements Node { id: ID! email: EmailAddr! + fullName: String! role: MembershipRole! expiresAt: Datetime! acceptedAt: Datetime @@ -2600,6 +2609,8 @@ input InviteMemberInput { organizationId: ID! email: EmailAddr! fullName: String! + role: MembershipRole! + createPeople: Boolean! } input UpdateMembershipInput { @@ -3245,6 +3256,11 @@ func (ec *executionContext) field_Organization_invitations_args(ctx context.Cont return nil, err } args["status"] = arg4 + arg5, err := graphql.ProcessArgField(ctx, rawArgs, "orderBy", ec.unmarshalOInvitationOrder2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐInvitationOrderBy) + if err != nil { + return nil, err + } + args["orderBy"] = arg5 return args, nil } @@ -4571,6 +4587,35 @@ func (ec *executionContext) fieldContext_Invitation_email(_ context.Context, fie return fc, nil } +func (ec *executionContext) _Invitation_fullName(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Invitation_fullName, + func(ctx context.Context) (any, error) { + return obj.FullName, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Invitation_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Invitation", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Invitation_role(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -4906,6 +4951,8 @@ func (ec *executionContext) fieldContext_InvitationEdge_node(_ context.Context, return ec.fieldContext_Invitation_id(ctx, field) case "email": return ec.fieldContext_Invitation_email(ctx, field) + case "fullName": + return ec.fieldContext_Invitation_fullName(ctx, field) case "role": return ec.fieldContext_Invitation_role(ctx, field) case "expiresAt": @@ -7637,7 +7684,7 @@ func (ec *executionContext) _Organization_invitations(ctx context.Context, field ec.fieldContext_Organization_invitations, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Organization().Invitations(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["status"].(*coredata.InvitationStatus)) + return ec.resolvers.Organization().Invitations(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["status"].(*coredata.InvitationStatus), fc.Args["orderBy"].(*types.InvitationOrderBy)) }, nil, ec.marshalOInvitationConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐInvitationConnection, @@ -12747,7 +12794,7 @@ func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "email", "fullName"} + fieldsInOrder := [...]string{"organizationId", "email", "fullName", "role", "createPeople"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -12775,6 +12822,20 @@ func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, return it, err } it.FullName = data + case "role": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) + data, err := ec.unmarshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole(ctx, v) + if err != nil { + return it, err + } + it.Role = data + case "createPeople": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("createPeople")) + data, err := ec.unmarshalNBoolean2bool(ctx, v) + if err != nil { + return it, err + } + it.CreatePeople = data } } @@ -14320,6 +14381,11 @@ func (ec *executionContext) _Invitation(ctx context.Context, sel ast.SelectionSe if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "fullName": + out.Values[i] = ec._Invitation_fullName(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "role": out.Values[i] = ec._Invitation_role(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/pkg/server/api/connect/v1/types/invitation.go b/pkg/server/api/connect/v1/types/invitation.go index 8b0cc5856..92042a0a6 100644 --- a/pkg/server/api/connect/v1/types/invitation.go +++ b/pkg/server/api/connect/v1/types/invitation.go @@ -67,6 +67,7 @@ func NewInvitation(invitation *coredata.Invitation) *Invitation { ID: invitation.ID, Email: invitation.Email, Role: invitation.Role, + FullName: invitation.FullName, ExpiresAt: invitation.ExpiresAt, AcceptedAt: invitation.AcceptedAt, CreatedAt: invitation.CreatedAt, diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index 3d072ac07..1b9369bbc 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -168,6 +168,7 @@ func (this Identity) GetID() gid.GID { return this.ID } type Invitation struct { ID gid.GID `json:"id"` Email mail.Addr `json:"email"` + FullName string `json:"fullName"` Role coredata.MembershipRole `json:"role"` ExpiresAt time.Time `json:"expiresAt"` AcceptedAt *time.Time `json:"acceptedAt,omitempty"` @@ -185,9 +186,11 @@ type InvitationEdge struct { } type InviteMemberInput struct { - OrganizationID gid.GID `json:"organizationId"` - Email mail.Addr `json:"email"` - FullName string `json:"fullName"` + OrganizationID gid.GID `json:"organizationId"` + Email mail.Addr `json:"email"` + FullName string `json:"fullName"` + Role coredata.MembershipRole `json:"role"` + CreatePeople bool `json:"createPeople"` } type InviteMemberPayload struct { diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 2557893d2..97828682f 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -1048,7 +1048,7 @@ func (r *organizationResolver) Members(ctx context.Context, obj *types.Organizat } // Invitations is the resolver for the invitations field. -func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) (*types.InvitationConnection, error) { +func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus, orderBy *types.InvitationOrderBy) (*types.InvitationConnection, error) { if gqlutils.OnlyTotalCountSelected(ctx) { return &types.InvitationConnection{ Resolver: r,