diff --git a/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts b/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts index 7cb5e6059..c8ac9a911 100644 --- a/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<14b3e1990be5f8333046dffcada56516>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -10,18 +10,16 @@ import { ConcreteRequest } from 'relay-runtime'; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; -export type InviteMemberInput = { - email: string; - fullName: string; +export type InviteUserInput = { organizationId: string; - role: MembershipRole; + profileId: string; }; export type InviteUserDialogMutation$variables = { connections: ReadonlyArray; - input: InviteMemberInput; + input: InviteUserInput; }; export type InviteUserDialogMutation$data = { - readonly inviteMember: { + readonly inviteUser: { readonly invitationEdge: { readonly node: { readonly acceptedAt: string | null | undefined; @@ -156,9 +154,9 @@ return { { "alias": null, "args": (v2/*: any*/), - "concreteType": "InviteMemberPayload", + "concreteType": "InviteUserPayload", "kind": "LinkedField", - "name": "inviteMember", + "name": "inviteUser", "plural": false, "selections": [ (v3/*: any*/) @@ -181,9 +179,9 @@ return { { "alias": null, "args": (v2/*: any*/), - "concreteType": "InviteMemberPayload", + "concreteType": "InviteUserPayload", "kind": "LinkedField", - "name": "inviteMember", + "name": "inviteUser", "plural": false, "selections": [ (v3/*: any*/), @@ -209,16 +207,16 @@ return { ] }, "params": { - "cacheID": "8d9e447c08739bef2460474225fb77bf", + "cacheID": "685e0bc12871b33065d1fe03334110b6", "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 canDelete: permission(action: \"iam:invitation:delete\")\n }\n }\n }\n}\n" + "text": "mutation InviteUserDialogMutation(\n $input: InviteUserInput!\n) {\n inviteUser(input: $input) {\n invitationEdge {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n canDelete: permission(action: \"iam:invitation:delete\")\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "dc2446f51b33dbab3a348e1250a25049"; +(node as any).hash = "70766e9b81d0ade0f128fae2e15aac62"; export default node; diff --git a/apps/console/src/__generated__/iam/PeopleListItem_removeMutation.graphql.ts b/apps/console/src/__generated__/iam/PeopleListItem_removeMutation.graphql.ts index 9383ba305..1b2c79ff1 100644 --- a/apps/console/src/__generated__/iam/PeopleListItem_removeMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/PeopleListItem_removeMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8c0cc8f8d9d0418ea1d817412793f2dc>> + * @generated SignedSource<<921243e6b4b9007688051434ab212f85>> * @lightSyntaxTransform * @nogrep */ @@ -9,17 +9,17 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type RemoveMemberInput = { - membershipId: string; +export type RemoveUserInput = { organizationId: string; + profileId: string; }; export type PeopleListItem_removeMutation$variables = { connections: ReadonlyArray; - input: RemoveMemberInput; + input: RemoveUserInput; }; export type PeopleListItem_removeMutation$data = { - readonly removeMember: { - readonly deletedMembershipId: string; + readonly removeUser: { + readonly deletedProfileId: string; } | null | undefined; }; export type PeopleListItem_removeMutation = { @@ -49,7 +49,7 @@ v3 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "deletedMembershipId", + "name": "deletedProfileId", "storageKey": null }; return { @@ -65,9 +65,9 @@ return { { "alias": null, "args": (v2/*: any*/), - "concreteType": "RemoveMemberPayload", + "concreteType": "RemoveUserPayload", "kind": "LinkedField", - "name": "removeMember", + "name": "removeUser", "plural": false, "selections": [ (v3/*: any*/) @@ -90,9 +90,9 @@ return { { "alias": null, "args": (v2/*: any*/), - "concreteType": "RemoveMemberPayload", + "concreteType": "RemoveUserPayload", "kind": "LinkedField", - "name": "removeMember", + "name": "removeUser", "plural": false, "selections": [ (v3/*: any*/), @@ -103,7 +103,7 @@ return { "handle": "deleteEdge", "key": "", "kind": "ScalarHandle", - "name": "deletedMembershipId", + "name": "deletedProfileId", "handleArgs": [ { "kind": "Variable", @@ -118,16 +118,16 @@ return { ] }, "params": { - "cacheID": "d5a0e195ee72906380f566bcecb45a3f", + "cacheID": "ec2aeab79c24169e2f8fc95ca3b28a7d", "id": null, "metadata": {}, "name": "PeopleListItem_removeMutation", "operationKind": "mutation", - "text": "mutation PeopleListItem_removeMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMembershipId\n }\n}\n" + "text": "mutation PeopleListItem_removeMutation(\n $input: RemoveUserInput!\n) {\n removeUser(input: $input) {\n deletedProfileId\n }\n}\n" } }; })(); -(node as any).hash = "c1bceb22b556aff77543ec1956445187"; +(node as any).hash = "e84b22e1f4a71940947b8a85d83a2dd6"; export default node; diff --git a/apps/console/src/__generated__/iam/EditMemberDialog_updateMutation.graphql.ts b/apps/console/src/__generated__/iam/PeopleListItem_updateRoleMutation.graphql.ts similarity index 72% rename from apps/console/src/__generated__/iam/EditMemberDialog_updateMutation.graphql.ts rename to apps/console/src/__generated__/iam/PeopleListItem_updateRoleMutation.graphql.ts index 737d88ee5..740b32d36 100644 --- a/apps/console/src/__generated__/iam/EditMemberDialog_updateMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/PeopleListItem_updateRoleMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<021e2d930ac4fe38c64cede962db93cf>> * @lightSyntaxTransform * @nogrep */ @@ -15,10 +15,10 @@ export type UpdateMembershipInput = { organizationId: string; role: MembershipRole; }; -export type EditMemberDialog_updateMutation$variables = { +export type PeopleListItem_updateRoleMutation$variables = { input: UpdateMembershipInput; }; -export type EditMemberDialog_updateMutation$data = { +export type PeopleListItem_updateRoleMutation$data = { readonly updateMembership: { readonly membership: { readonly id: string; @@ -26,9 +26,9 @@ export type EditMemberDialog_updateMutation$data = { }; }; }; -export type EditMemberDialog_updateMutation = { - response: EditMemberDialog_updateMutation$data; - variables: EditMemberDialog_updateMutation$variables; +export type PeopleListItem_updateRoleMutation = { + response: PeopleListItem_updateRoleMutation$data; + variables: PeopleListItem_updateRoleMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -88,7 +88,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "EditMemberDialog_updateMutation", + "name": "PeopleListItem_updateRoleMutation", "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null @@ -97,20 +97,20 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "EditMemberDialog_updateMutation", + "name": "PeopleListItem_updateRoleMutation", "selections": (v1/*: any*/) }, "params": { - "cacheID": "696ec9f74220b92171bf60630e5c8d7f", + "cacheID": "d15bdc1ff79729c3878c0cef9095a241", "id": null, "metadata": {}, - "name": "EditMemberDialog_updateMutation", + "name": "PeopleListItem_updateRoleMutation", "operationKind": "mutation", - "text": "mutation EditMemberDialog_updateMutation(\n $input: UpdateMembershipInput!\n) {\n updateMembership(input: $input) {\n membership {\n id\n role\n }\n }\n}\n" + "text": "mutation PeopleListItem_updateRoleMutation(\n $input: UpdateMembershipInput!\n) {\n updateMembership(input: $input) {\n membership {\n id\n role\n }\n }\n}\n" } }; })(); -(node as any).hash = "fc971ff69092912586c14c7f6e2111ed"; +(node as any).hash = "05245fe86928cb4afa5a958835ad167e"; export default node; diff --git a/apps/console/src/__generated__/iam/PeoplePageQuery.graphql.ts b/apps/console/src/__generated__/iam/PeoplePageQuery.graphql.ts index dfd9be2a3..2d9ab1102 100644 --- a/apps/console/src/__generated__/iam/PeoplePageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/PeoplePageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7f025767ad277b8b8de6cbe9b1490312>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -16,6 +16,7 @@ export type PeoplePageQuery$variables = { export type PeoplePageQuery$data = { readonly organization: { readonly __typename: "Organization"; + readonly canCreateUser: boolean; readonly canInviteUser: boolean; readonly invitations: { readonly $updatableFragmentSpreads: FragmentRefs<"PeoplePage_invitationsTotalCountFragment">; @@ -59,6 +60,19 @@ v2 = { "storageKey": null }, v3 = { + "alias": "canCreateUser", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership-profile:create" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership-profile:create\")" +}, +v4 = { "alias": "canInviteUser", "args": [ { @@ -71,85 +85,85 @@ v3 = { "name": "permission", "storageKey": "permission(action:\"iam:invitation:create\")" }, -v4 = { +v5 = { "kind": "Literal", "name": "first", "value": 20 }, -v5 = { +v6 = { "direction": "ASC", "field": "FULL_NAME" }, -v6 = [ - (v4/*: any*/), +v7 = [ + (v5/*: any*/), { "kind": "Literal", "name": "orderBy", - "value": (v5/*: any*/) + "value": (v6/*: any*/) } ], -v7 = { +v8 = { "alias": null, "args": null, "kind": "ScalarField", "name": "totalCount", "storageKey": null }, -v8 = { +v9 = { "direction": "DESC", "field": "CREATED_AT" }, -v9 = [ - (v4/*: any*/), +v10 = [ + (v5/*: any*/), { "kind": "Literal", "name": "orderBy", - "value": (v8/*: any*/) + "value": (v9/*: any*/) } ], -v10 = { +v11 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v11 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v12 = { +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "role", "storageKey": null }, -v13 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "email", "storageKey": null }, -v14 = { +v15 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v15 = { +v16 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v16 = { +v17 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -188,7 +202,7 @@ v16 = { ], "storageKey": null }, -v17 = { +v18 = { "kind": "ClientExtension", "selections": [ { @@ -200,7 +214,7 @@ v17 = { } ] }, -v18 = [ +v19 = [ "orderBy" ]; return { @@ -225,17 +239,18 @@ return { "kind": "InlineFragment", "selections": [ (v3/*: any*/), + (v4/*: any*/), { "kind": "RequiredField", "field": { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": "ProfileConnection", "kind": "LinkedField", "name": "profiles", "plural": false, "selections": [ - (v7/*: any*/) + (v8/*: any*/) ], "storageKey": "profiles(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})" }, @@ -243,11 +258,11 @@ return { }, { "args": [ - (v4/*: any*/), + (v5/*: any*/), { "kind": "Literal", "name": "order", - "value": (v5/*: any*/) + "value": (v6/*: any*/) } ], "kind": "FragmentSpread", @@ -257,13 +272,13 @@ return { "kind": "RequiredField", "field": { "alias": null, - "args": (v9/*: any*/), + "args": (v10/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "invitations", "plural": false, "selections": [ - (v7/*: any*/), + (v8/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -276,11 +291,11 @@ return { }, { "args": [ - (v4/*: any*/), + (v5/*: any*/), { "kind": "Literal", "name": "order", - "value": (v8/*: any*/) + "value": (v9/*: any*/) } ], "kind": "FragmentSpread", @@ -314,20 +329,21 @@ return { "plural": false, "selections": [ (v2/*: any*/), - (v10/*: any*/), + (v11/*: any*/), { "kind": "InlineFragment", "selections": [ (v3/*: any*/), + (v4/*: any*/), { "alias": null, - "args": (v6/*: any*/), + "args": (v7/*: any*/), "concreteType": "ProfileConnection", "kind": "LinkedField", "name": "profiles", "plural": false, "selections": [ - (v7/*: any*/), + (v8/*: any*/), { "alias": null, "args": null, @@ -344,7 +360,7 @@ return { "name": "node", "plural": false, "selections": [ - (v10/*: any*/), + (v11/*: any*/), { "alias": null, "args": null, @@ -359,7 +375,7 @@ return { "name": "state", "storageKey": null }, - (v11/*: any*/), + (v12/*: any*/), { "alias": null, "args": null, @@ -382,8 +398,8 @@ return { "name": "membership", "plural": false, "selections": [ - (v10/*: any*/), - (v12/*: any*/), + (v11/*: any*/), + (v13/*: any*/), { "alias": "canUpdate", "args": [ @@ -421,12 +437,12 @@ return { "name": "identity", "plural": false, "selections": [ - (v13/*: any*/), - (v10/*: any*/) + (v14/*: any*/), + (v11/*: any*/) ], "storageKey": null }, - (v14/*: any*/), + (v15/*: any*/), { "alias": "canUpdate", "args": [ @@ -444,19 +460,19 @@ return { ], "storageKey": null }, - (v15/*: any*/) + (v16/*: any*/) ], "storageKey": null }, - (v16/*: any*/), - (v17/*: any*/) + (v17/*: any*/), + (v18/*: any*/) ], "storageKey": "profiles(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})" }, { "alias": null, - "args": (v6/*: any*/), - "filters": (v18/*: any*/), + "args": (v7/*: any*/), + "filters": (v19/*: any*/), "handle": "connection", "key": "PeopleListFragment_profiles", "kind": "LinkedHandle", @@ -464,13 +480,13 @@ return { }, { "alias": null, - "args": (v9/*: any*/), + "args": (v10/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "invitations", "plural": false, "selections": [ - (v7/*: any*/), + (v8/*: any*/), (v2/*: any*/), { "alias": null, @@ -488,10 +504,10 @@ return { "name": "node", "plural": false, "selections": [ - (v10/*: any*/), (v11/*: any*/), - (v13/*: any*/), (v12/*: any*/), + (v14/*: any*/), + (v13/*: any*/), { "alias": null, "args": null, @@ -499,7 +515,7 @@ return { "name": "status", "storageKey": null }, - (v14/*: any*/), + (v15/*: any*/), { "alias": null, "args": null, @@ -524,19 +540,19 @@ return { ], "storageKey": null }, - (v15/*: any*/) + (v16/*: any*/) ], "storageKey": null }, - (v16/*: any*/), - (v17/*: any*/) + (v17/*: any*/), + (v18/*: any*/) ], "storageKey": "invitations(first:20,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v9/*: any*/), - "filters": (v18/*: any*/), + "args": (v10/*: any*/), + "filters": (v19/*: any*/), "handle": "connection", "key": "InvitationListFragment_invitations", "kind": "LinkedHandle", @@ -552,16 +568,16 @@ return { ] }, "params": { - "cacheID": "b67de1f40d556a5e4a6d78104e2eb224", + "cacheID": "3aef3e0e345bd04d2432c7167c87aa8d", "id": null, "metadata": {}, "name": "PeoplePageQuery", "operationKind": "query", - "text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canInviteUser: permission(action: \"iam:invitation:create\")\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...PeopleListFragment_8lnpd\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n ...InvitationListFragment_1PypFi\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, 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 acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n kind\n position\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" + "text": "query PeoplePageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canCreateUser: permission(action: \"iam:membership-profile:create\")\n canInviteUser: permission(action: \"iam:invitation:create\")\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n }\n ...PeopleListFragment_8lnpd\n invitations(first: 20, orderBy: {direction: DESC, field: CREATED_AT}) {\n totalCount\n __typename\n }\n ...InvitationListFragment_1PypFi\n }\n id\n }\n}\n\nfragment InvitationListFragment_1PypFi on Organization {\n invitations(first: 20, orderBy: {direction: DESC, 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 acceptedAt\n canDelete: permission(action: \"iam:invitation:delete\")\n}\n\nfragment PeopleListFragment_8lnpd on Organization {\n profiles(first: 20, orderBy: {direction: ASC, field: FULL_NAME}) {\n totalCount\n edges {\n node {\n id\n ...PeopleListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n kind\n position\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" } }; })(); -(node as any).hash = "4cb0d9ec65b3a55eb1decb4de36ac2d8"; +(node as any).hash = "a89fa762bef0dcc31d05543a782f9aaf"; export default node; diff --git a/apps/console/src/__generated__/iam/PersonFormFragment.graphql.ts b/apps/console/src/__generated__/iam/PersonFormFragment.graphql.ts index 9295ee92f..ee5260ddb 100644 --- a/apps/console/src/__generated__/iam/PersonFormFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/PersonFormFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<6ee89b2ab0c55ac9498bf9186e78ee90>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,6 +9,7 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; import { FragmentRefs } from "relay-runtime"; export type PersonFormFragment$data = { @@ -16,9 +17,13 @@ export type PersonFormFragment$data = { readonly canUpdate: boolean; readonly contractEndDate: string | null | undefined; readonly contractStartDate: string | null | undefined; + readonly emailAddress: string; readonly fullName: string; readonly id: string; readonly kind: ProfileKind; + readonly membership: { + readonly role: MembershipRole; + }; readonly position: string | null | undefined; readonly " $fragmentType": "PersonFormFragment"; }; @@ -47,6 +52,35 @@ const node: ReaderFragment = { "name": "fullName", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailAddress", + "storageKey": null + }, + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "membership", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + }, { "alias": null, "args": null, @@ -100,6 +134,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "9ecc41c74271a3202bbde08b63c92b73"; +(node as any).hash = "4f99d9b9ed708290c5a7d7ec258a0b1d"; export default node; diff --git a/apps/console/src/__generated__/iam/PersonForm_createMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonForm_createMutation.graphql.ts new file mode 100644 index 000000000..a85d55540 --- /dev/null +++ b/apps/console/src/__generated__/iam/PersonForm_createMutation.graphql.ts @@ -0,0 +1,314 @@ +/** + * @generated SignedSource<<5148e84724d7649be7a0ccf0fe813893>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; +export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; +export type CreateUserInput = { + additionalEmailAddresses?: ReadonlyArray | null | undefined; + contractEndDate?: string | null | undefined; + contractStartDate?: string | null | undefined; + emailAddress: string; + fullName: string; + kind: ProfileKind; + organizationId: string; + position?: string | null | undefined; + role: MembershipRole; +}; +export type PersonForm_createMutation$variables = { + connections: ReadonlyArray; + input: CreateUserInput; +}; +export type PersonForm_createMutation$data = { + readonly createUser: { + readonly profileEdge: { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"PeopleListItemFragment">; + }; + }; + } | null | undefined; +}; +export type PersonForm_createMutation = { + response: PersonForm_createMutation$data; + variables: PersonForm_createMutation$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": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "PersonForm_createMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateUserPayload", + "kind": "LinkedField", + "name": "createUser", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ProfileEdge", + "kind": "LinkedField", + "name": "profileEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Profile", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "PeopleListItemFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "PersonForm_createMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateUserPayload", + "kind": "LinkedField", + "name": "createUser", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ProfileEdge", + "kind": "LinkedField", + "name": "profileEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Profile", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "source", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "kind", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "position", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "membership", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership:update\")" + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership-profile:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership-profile:delete\")" + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "identity", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:membership-profile:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:membership-profile:update\")" + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "profileEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "ff3f29c40614a067d108d55df828f7c0", + "id": null, + "metadata": {}, + "name": "PersonForm_createMutation", + "operationKind": "mutation", + "text": "mutation PersonForm_createMutation(\n $input: CreateUserInput!\n) {\n createUser(input: $input) {\n profileEdge {\n node {\n ...PeopleListItemFragment\n id\n }\n }\n }\n}\n\nfragment PeopleListItemFragment on Profile {\n id\n source\n state\n fullName\n kind\n position\n membership {\n id\n role\n canUpdate: permission(action: \"iam:membership:update\")\n canDelete: permission(action: \"iam:membership-profile:delete\")\n }\n identity {\n email\n id\n }\n createdAt\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" + } +}; +})(); + +(node as any).hash = "8dcc7d2920cc0196f71ecce6e78703c4"; + +export default node; diff --git a/apps/console/src/__generated__/iam/PersonFormMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonForm_updateMutation.graphql.ts similarity index 68% rename from apps/console/src/__generated__/iam/PersonFormMutation.graphql.ts rename to apps/console/src/__generated__/iam/PersonForm_updateMutation.graphql.ts index 9d9c12d97..04af2a391 100644 --- a/apps/console/src/__generated__/iam/PersonFormMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/PersonForm_updateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<8563f3b122bb8d5b6c2e5e7354a0a0c8>> * @lightSyntaxTransform * @nogrep */ @@ -10,7 +10,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; -export type UpdateProfileInput = { +export type UpdateUserInput = { additionalEmailAddresses?: ReadonlyArray | null | undefined; contractEndDate?: string | null | undefined; contractStartDate?: string | null | undefined; @@ -19,19 +19,19 @@ export type UpdateProfileInput = { kind: ProfileKind; position?: string | null | undefined; }; -export type PersonFormMutation$variables = { - input: UpdateProfileInput; +export type PersonForm_updateMutation$variables = { + input: UpdateUserInput; }; -export type PersonFormMutation$data = { - readonly updateProfile: { +export type PersonForm_updateMutation$data = { + readonly updateUser: { readonly profile: { readonly id: string; }; }; }; -export type PersonFormMutation = { - response: PersonFormMutation$data; - variables: PersonFormMutation$variables; +export type PersonForm_updateMutation = { + response: PersonForm_updateMutation$data; + variables: PersonForm_updateMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -52,9 +52,9 @@ v1 = [ "variableName": "input" } ], - "concreteType": "UpdateProfilePayload", + "concreteType": "UpdateUserPayload", "kind": "LinkedField", - "name": "updateProfile", + "name": "updateUser", "plural": false, "selections": [ { @@ -84,7 +84,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "PersonFormMutation", + "name": "PersonForm_updateMutation", "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null @@ -93,20 +93,20 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "PersonFormMutation", + "name": "PersonForm_updateMutation", "selections": (v1/*: any*/) }, "params": { - "cacheID": "fe068a78bed066f0d50cab1dde6db292", + "cacheID": "0623be6bdbd6575b04173a59b1dd45f7", "id": null, "metadata": {}, - "name": "PersonFormMutation", + "name": "PersonForm_updateMutation", "operationKind": "mutation", - "text": "mutation PersonFormMutation(\n $input: UpdateProfileInput!\n) {\n updateProfile(input: $input) {\n profile {\n id\n }\n }\n}\n" + "text": "mutation PersonForm_updateMutation(\n $input: UpdateUserInput!\n) {\n updateUser(input: $input) {\n profile {\n id\n }\n }\n}\n" } }; })(); -(node as any).hash = "eb43fe5003e9761924aad474bf8296ee"; +(node as any).hash = "e1da3d7148eef5f6ab8bdf827e41654a"; export default node; diff --git a/apps/console/src/__generated__/iam/PersonPageQuery.graphql.ts b/apps/console/src/__generated__/iam/PersonPageQuery.graphql.ts index e3ff4cad0..592bfbc4e 100644 --- a/apps/console/src/__generated__/iam/PersonPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/PersonPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<2e75f19fcdaf2b78a187c33458c9cfe0>> + * @generated SignedSource<<679cd9698980b9fdf8b330df8df3aac5>> * @lightSyntaxTransform * @nogrep */ @@ -18,12 +18,10 @@ export type PersonPageQuery$data = { readonly __typename: "Profile"; readonly canDelete: boolean; readonly fullName: string; + readonly id: string; readonly identity: { readonly email: string; }; - readonly membership: { - readonly id: string; - }; readonly " $fragmentSpreads": FragmentRefs<"PersonFormFragment">; } | { // This will never be '%other', but we need some @@ -62,36 +60,24 @@ v3 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "fullName", + "name": "id", "storageKey": null }, v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "id", + "name": "fullName", "storageKey": null }, v5 = { - "alias": null, - "args": null, - "concreteType": "Membership", - "kind": "LinkedField", - "name": "membership", - "plural": false, - "selections": [ - (v4/*: any*/) - ], - "storageKey": null -}, -v6 = { "alias": null, "args": null, "kind": "ScalarField", "name": "email", "storageKey": null }, -v7 = { +v6 = { "alias": "canDelete", "args": [ { @@ -126,11 +112,7 @@ return { "kind": "InlineFragment", "selections": [ (v3/*: any*/), - { - "kind": "RequiredField", - "field": (v5/*: any*/), - "action": "THROW" - }, + (v4/*: any*/), { "kind": "RequiredField", "field": { @@ -141,13 +123,13 @@ return { "name": "identity", "plural": false, "selections": [ - (v6/*: any*/) + (v5/*: any*/) ], "storageKey": null }, "action": "THROW" }, - (v7/*: any*/), + (v6/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -181,12 +163,11 @@ return { "plural": false, "selections": [ (v2/*: any*/), - (v4/*: any*/), + (v3/*: any*/), { "kind": "InlineFragment", "selections": [ - (v3/*: any*/), - (v5/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -195,12 +176,38 @@ return { "name": "identity", "plural": false, "selections": [ - (v6/*: any*/), - (v4/*: any*/) + (v5/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + }, + (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "emailAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "membership", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "role", + "storageKey": null + }, + (v3/*: any*/) ], "storageKey": null }, - (v7/*: any*/), { "alias": null, "args": null, @@ -259,16 +266,16 @@ return { ] }, "params": { - "cacheID": "e148eb605cc90b57c6c7772b9045217d", + "cacheID": "cb1cec2fd7c66b42f61d40e6a22b0d67", "id": null, "metadata": {}, "name": "PersonPageQuery", "operationKind": "query", - "text": "query PersonPageQuery(\n $personId: ID!\n) {\n person: node(id: $personId) {\n __typename\n ... on Profile {\n fullName\n membership {\n id\n }\n identity {\n email\n id\n }\n canDelete: permission(action: \"iam:membership-profile:delete\")\n ...PersonFormFragment\n }\n id\n }\n}\n\nfragment PersonFormFragment on Profile {\n id\n fullName\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" + "text": "query PersonPageQuery(\n $personId: ID!\n) {\n person: node(id: $personId) {\n __typename\n ... on Profile {\n id\n fullName\n identity {\n email\n id\n }\n canDelete: permission(action: \"iam:membership-profile:delete\")\n ...PersonFormFragment\n }\n id\n }\n}\n\nfragment PersonFormFragment on Profile {\n id\n fullName\n emailAddress\n membership {\n role\n id\n }\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n canUpdate: permission(action: \"iam:membership-profile:update\")\n}\n" } }; })(); -(node as any).hash = "5dce91fda9224f6f3928d4549485d3bd"; +(node as any).hash = "c2954bc62ed4a3e3a9b1ca9596b68e1c"; export default node; diff --git a/apps/console/src/__generated__/iam/PersonPage_removeMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonPage_removeMutation.graphql.ts index f3cf6401c..2996120e8 100644 --- a/apps/console/src/__generated__/iam/PersonPage_removeMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/PersonPage_removeMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<0c55769185354921be98596538492fab>> + * @generated SignedSource<<7df320d2cbda9f48737622295ed141ad>> * @lightSyntaxTransform * @nogrep */ @@ -9,16 +9,16 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type RemoveMemberInput = { - membershipId: string; +export type RemoveUserInput = { organizationId: string; + profileId: string; }; export type PersonPage_removeMutation$variables = { - input: RemoveMemberInput; + input: RemoveUserInput; }; export type PersonPage_removeMutation$data = { - readonly removeMember: { - readonly deletedMembershipId: string; + readonly removeUser: { + readonly deletedProfileId: string; } | null | undefined; }; export type PersonPage_removeMutation = { @@ -44,16 +44,16 @@ v1 = [ "variableName": "input" } ], - "concreteType": "RemoveMemberPayload", + "concreteType": "RemoveUserPayload", "kind": "LinkedField", - "name": "removeMember", + "name": "removeUser", "plural": false, "selections": [ { "alias": null, "args": null, "kind": "ScalarField", - "name": "deletedMembershipId", + "name": "deletedProfileId", "storageKey": null } ], @@ -78,16 +78,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "0c220926bf64fda6954b3ab8e23dcd72", + "cacheID": "1e686e9dcb1425cde221721b4635a747", "id": null, "metadata": {}, "name": "PersonPage_removeMutation", "operationKind": "mutation", - "text": "mutation PersonPage_removeMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMembershipId\n }\n}\n" + "text": "mutation PersonPage_removeMutation(\n $input: RemoveUserInput!\n) {\n removeUser(input: $input) {\n deletedProfileId\n }\n}\n" } }; })(); -(node as any).hash = "60608384ce1ea37ec7b347ec4fdb4a61"; +(node as any).hash = "37b79997d660eee761e9380612739211"; export default node; diff --git a/apps/console/src/pages/iam/organizations/people/PeoplePage.tsx b/apps/console/src/pages/iam/organizations/people/PeoplePage.tsx index 88ee3e697..6ff7c85e4 100644 --- a/apps/console/src/pages/iam/organizations/people/PeoplePage.tsx +++ b/apps/console/src/pages/iam/organizations/people/PeoplePage.tsx @@ -2,11 +2,12 @@ import { useTranslate } from "@probo/i18n"; import { Button, PageHeader, TabBadge, TabItem, Tabs } from "@probo/ui"; import { useState } from "react"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; -import { ConnectionHandler, graphql } from "relay-runtime"; +import { ConnectionHandler, type DataID, graphql } from "relay-runtime"; import type { PeoplePageQuery } from "#/__generated__/iam/PeoplePageQuery.graphql"; import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { AddPersonDialog } from "./_components/AddPersonDialog"; import { InvitationList } from "./_components/InvitationList"; import { InviteUserDialog } from "./_components/InviteUserDialog"; import { PeopleList } from "./_components/PeopleList"; @@ -16,6 +17,7 @@ export const peoplePageQuery = graphql` organization: node(id: $organizationId) @required(action: THROW) { __typename ... on Organization { + canCreateUser: permission(action: "iam:membership-profile:create") canInviteUser: permission(action: "iam:invitation:create") profiles(first: 20, orderBy: { direction: ASC, field: FULL_NAME }) @required(action: THROW) { @@ -53,6 +55,13 @@ export function PeoplePage(props: { const [activeTab, setActiveTab] = useState<"people" | "invitations">( "people", ); + const [connectionId, setConnectionId] = useState( + ConnectionHandler.getConnectionID( + organizationId, + "PeopleListFragment_profiles", + { orderBy: { direction: "ASC", field: "CREATED_AT" } }, + ), + ); const { organization } = usePreloadedQuery( peoplePageQuery, @@ -73,6 +82,12 @@ export function PeoplePage(props: { return (
+ {organization.canCreateUser + && ( + + + + )} {organization.canInviteUser && (
- {activeTab === "people" && } + {activeTab === "people" && ( + + )} {activeTab === "invitations" && ( }) throw new Error("invalid type for node"); } - const [removeMembership, isRemoving] = useMutationWithToasts( - removeMemberMutation, + const [removeUser, isRemoving] = useMutationWithToasts( + removeUserMutation, { - successMessage: __("Member removed successfully"), - errorMessage: __("Failed to remove member"), + successMessage: __("Person removed successfully"), + errorMessage: __("Failed to remove person"), }, ); const handleRemove = () => { confirm( () => { - return removeMembership({ + return removeUser({ variables: { input: { - membershipId: person.membership.id, + profileId: person.id, organizationId: organizationId, }, }, @@ -121,7 +119,9 @@ export function PersonPage(props: { queryRef: PreloadedQuery }) )}
- + + +
); }; diff --git a/apps/console/src/pages/iam/organizations/people/_components/AddPersonDialog.tsx b/apps/console/src/pages/iam/organizations/people/_components/AddPersonDialog.tsx new file mode 100644 index 000000000..c0190fdf6 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/people/_components/AddPersonDialog.tsx @@ -0,0 +1,26 @@ +import { useTranslate } from "@probo/i18n"; +import { Dialog } from "@probo/ui"; +import { type PropsWithChildren } from "react"; +import type { DataID } from "relay-runtime"; + +import { PersonForm } from "./PersonForm"; + +export function AddPersonDialog(props: PropsWithChildren<{ + connectionId: DataID; +}>) { + const { children, connectionId } = props; + + const { __ } = useTranslate(); + + return ( + +
+ +
+
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/people/_components/InviteUserDialog.tsx b/apps/console/src/pages/iam/organizations/people/_components/InviteUserDialog.tsx index 80621a8e3..3f5176546 100644 --- a/apps/console/src/pages/iam/organizations/people/_components/InviteUserDialog.tsx +++ b/apps/console/src/pages/iam/organizations/people/_components/InviteUserDialog.tsx @@ -26,10 +26,10 @@ import { invitationCountFragment } from "../PeoplePage"; const inviteMutation = graphql` mutation InviteUserDialogMutation( - $input: InviteMemberInput! + $input: InviteUserInput! $connections: [ID!]! ) { - inviteMember(input: $input) { + inviteUser(input: $input) { invitationEdge @prependEdge(connections: $connections) { node { id diff --git a/apps/console/src/pages/iam/organizations/people/_components/PeopleList.tsx b/apps/console/src/pages/iam/organizations/people/_components/PeopleList.tsx index 5298620a2..d60a3f707 100644 --- a/apps/console/src/pages/iam/organizations/people/_components/PeopleList.tsx +++ b/apps/console/src/pages/iam/organizations/people/_components/PeopleList.tsx @@ -1,13 +1,14 @@ import { useTranslate } from "@probo/i18n"; import { Tbody, Td, Th, Thead, Tr } from "@probo/ui"; import type { ComponentProps } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; +import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay"; import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql"; import type { PeopleListFragment_RefetchQuery } from "#/__generated__/iam/PeopleListFragment_RefetchQuery.graphql"; -import { SortableTable, SortableTh } from "#/components/SortableTable"; +import { type Order, SortableTable, SortableTh } from "#/components/SortableTable"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; -import { MemberListItem } from "./PeopleListItem"; +import { PeopleListItem } from "./PeopleListItem"; const fragment = graphql` fragment PeopleListFragment on Organization @@ -41,9 +42,13 @@ const fragment = graphql` } `; -export function PeopleList(props: { fKey: PeopleListFragment$key }) { - const { fKey } = props; +export function PeopleList(props: { + onConnectionIdChange: (connectionId: string) => void; + fKey: PeopleListFragment$key; +}) { + const { fKey, onConnectionIdChange } = props; + const organizationId = useOrganizationId(); const { __ } = useTranslate(); const peoplePagination = usePaginationFragment< @@ -55,6 +60,16 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) { peoplePagination.refetch({}, { fetchPolicy: "network-only" }); }; + const handleOrderChange = (order: Order) => { + onConnectionIdChange( + ConnectionHandler.getConnectionID( + organizationId, + "PeopleListFragment_profiles", + { orderBy: order }, + ), + ); + }; + return ( - {__("Name")} - {__("Email")} + {__("Name")} + {__("Email")} {__("Type")} - {__("Role")} - {__("Joined")} + {__("Role")} + {__("Joined")} {__("Position")} @@ -87,7 +102,7 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) { ) : ( peoplePagination.data.profiles.edges.map(({ node: profile }) => ( - void; @@ -64,27 +75,48 @@ export function MemberListItem(props: { const organizationId = useOrganizationId(); const { __ } = useTranslate(); const confirm = useConfirm(); - const [dialogOpen, setDialogOpen] = useState(false); + + const { role } = use(CurrentUser); + const availableRoles = getAssignableRoles(role); const profile = useFragment(fragment, fKey); const isInactive = profile.state === "INACTIVE"; - const [removeMembership, isRemoving] = useMutationWithToasts( - removeMemberMutation, + const [updateMembership, isUpdatingRole] = useMutationWithToasts( + updateRoleMutation, { - successMessage: __("Member removed successfully"), - errorMessage: __("Failed to remove member"), + successMessage: __("Role updated successfully"), + errorMessage: __("Failed to update role"), }, ); + const [removeUser, isRemoving] = useMutationWithToasts( + removeUserMutation, + { + successMessage: __("Person removed successfully"), + errorMessage: __("Failed to remove person"), + }, + ); + + const handleUpdateRole = async (role: string) => { + await updateMembership({ + variables: { + input: { + membershipId: profile.membership.id, + organizationId: organizationId, + role: role, + }, + }, + }); + }; const handleRemove = () => { confirm( () => { - return removeMembership({ + return removeUser({ variables: { input: { - membershipId: profile.membership.id, + profileId: profile.id, organizationId: organizationId, }, connections: [connectionId], @@ -122,8 +154,28 @@ export function MemberListItem(props: { {profile.kind} - - {profile.membership.role} + + {new Date(profile.createdAt).toLocaleDateString()} {profile.position} @@ -133,15 +185,6 @@ export function MemberListItem(props: { className="flex gap-2 justify-end" onClick={e => e.stopPropagation()} > - {profile.membership.canUpdate && ( - )} ); } + +export function PersonFormLoader(props: { fragmentRef: PersonFormFragment$key }) { + const { fragmentRef } = props; + + const person = useFragment(fragment, fragmentRef); + + return ( + + ); +} diff --git a/apps/console/src/pages/iam/organizations/settings/_components/EditMemberDialog.tsx b/apps/console/src/pages/iam/organizations/settings/_components/EditMemberDialog.tsx deleted file mode 100644 index c971575fd..000000000 --- a/apps/console/src/pages/iam/organizations/settings/_components/EditMemberDialog.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { getAssignableRoles, sprintf } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { - Button, - Dialog, - DialogContent, - DialogFooter, - Field, - Option, - Select, - Spinner, -} from "@probo/ui"; -import { use, useState } from "react"; -import { graphql } from "relay-runtime"; - -import type { PeopleListItemFragment$data } from "#/__generated__/iam/PeopleListItemFragment.graphql"; -import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; -import { useOrganizationId } from "#/hooks/useOrganizationId"; -import { CurrentUser } from "#/providers/CurrentUser"; - -const updateMembershipMutation = graphql` - mutation EditMemberDialog_updateMutation($input: UpdateMembershipInput!) { - updateMembership(input: $input) { - membership { - id - role - } - } - } -`; - -export function EditMemberDialog(props: { - profile: PeopleListItemFragment$data; - onClose: () => void; -}) { - const { profile, onClose } = props; - - const organizationId = useOrganizationId(); - const { __ } = useTranslate(); - - const { role } = use(CurrentUser); - const availableRoles = getAssignableRoles(role); - - const [selectedRole, setSelectedRole] = useState(profile.membership.role); - - const [updateMembership, isUpdating] = useMutationWithToasts( - updateMembershipMutation, - { - successMessage: __("Role updated successfully"), - errorMessage: __("Failed to update role"), - }, - ); - - const handleUpdateRole = async (e: React.FormEvent) => { - e.preventDefault(); - await updateMembership({ - variables: { - input: { - membershipId: profile.membership.id, - organizationId: organizationId, - role: selectedRole, - }, - }, - onCompleted: onClose, - }); - }; - - return ( - -
void handleUpdateRole(e)}> - -
-

- {sprintf( - __("Update the role for %s"), - profile.fullName, - )} -

- - - - - -
- {selectedRole === "OWNER" && ( -

{__("Full access to everything")}

- )} - {selectedRole === "ADMIN" && ( -

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

- )} - {selectedRole === "VIEWER" &&

{__("Read-only access")}

} - {selectedRole === "AUDITOR" && ( -

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

- )} - {selectedRole === "EMPLOYEE" && ( -

{__("Access to employee page")}

- )} -
-
-
- - - -
-
- ); -} diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index 90bbe1d79..6b938c84d 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -78,7 +78,7 @@ export { } from "./date"; export { getTrustCenterUrl } from "./trustCenter"; export { formatError, type GraphQLError } from "./error"; -export { Role, getAssignableRoles } from "./roles"; +export { Role, roles, getAssignableRoles } from "./roles"; export { getTrustCenterDocumentAccessInfo, getTrustCenterDocumentAccessStatusBadgeVariant, diff --git a/packages/helpers/src/roles.ts b/packages/helpers/src/roles.ts index c2bfdeb36..bca0f80eb 100644 --- a/packages/helpers/src/roles.ts +++ b/packages/helpers/src/roles.ts @@ -7,6 +7,13 @@ export const Role = { } as const export type Role = (typeof Role)[keyof typeof Role]; +export const roles = [ + "OWNER", + "ADMIN", + "VIEWER", + "AUDITOR", + "EMPLOYEE", +] as const export function getAssignableRoles(currentRole: Role): Role[] { if (currentRole === Role.OWNER) { diff --git a/pkg/coredata/migrations/20260212T163603Z.sql b/pkg/coredata/migrations/20260212T163603Z.sql index 9c8413b86..54387b13c 100644 --- a/pkg/coredata/migrations/20260212T163603Z.sql +++ b/pkg/coredata/migrations/20260212T163603Z.sql @@ -18,6 +18,11 @@ WHERE ALTER TABLE iam_membership_profiles DROP COLUMN membership_id; +ALTER TABLE + iam_memberships +ALTER COLUMN + source DROP NOT NULL; + ALTER TABLE iam_scim_events ADD @@ -29,7 +34,7 @@ WITH emails AS ( m.id FROM iam_memberships m - INNER JOIN identities i ON i.id = m.identity_id + INNER JOIN identities i ON i.id = m.identity_id ) UPDATE iam_scim_events se diff --git a/pkg/iam/iam_policies.go b/pkg/iam/iam_policies.go index 2c1c46b2f..7ac624fbf 100644 --- a/pkg/iam/iam_policies.go +++ b/pkg/iam/iam_policies.go @@ -164,7 +164,9 @@ var IAMOwnerPolicy = policy.NewPolicy( policy.Allow( ActionMembershipProfileGet, ActionMembershipProfileList, + ActionMembershipProfileCreate, ActionMembershipProfileUpdate, + ActionMembershipProfileDelete, ). WithSID("full-membership-profile-access"). When(policy.Equals("principal.organization_id", "resource.organization_id")), @@ -243,7 +245,9 @@ var IAMAdminPolicy = policy.NewPolicy( policy.Allow( ActionMembershipProfileGet, ActionMembershipProfileList, + ActionMembershipProfileCreate, ActionMembershipProfileUpdate, + ActionMembershipProfileDelete, ). WithSID("membership-profile-admin-access"). When(policy.Equals("principal.organization_id", "resource.organization_id")), diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index bedb6afc3..bb4304076 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -942,7 +942,7 @@ func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserReq now = time.Now() ) - err := s.pg.WithConn( + err := s.pg.WithTx( ctx, func(conn pg.Conn) error { identity := &coredata.Identity{} @@ -966,6 +966,9 @@ func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserReq profile = &coredata.MembershipProfile{ ID: gid.New(req.OrganizationID.TenantID(), coredata.MembershipProfileEntityType), + IdentityID: identity.ID, + OrganizationID: req.OrganizationID, + Source: coredata.ProfileSourceManual, FullName: req.FullName, Kind: req.Kind, AdditionalEmailAddresses: req.AdditionalEmailAddresses, diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 626eee62d..6d86e9535 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -183,6 +183,7 @@ type Identity implements Node { type Profile implements Node { id: ID! fullName: String! + emailAddress: EmailAddr! source: String! state: ProfileState! additionalEmailAddresses: [EmailAddr!]! diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 22d50e00c..378371fb4 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -294,6 +294,7 @@ type ComplexityRoot struct { ContractEndDate func(childComplexity int) int ContractStartDate func(childComplexity int) int CreatedAt func(childComplexity int) int + EmailAddress func(childComplexity int) int FullName func(childComplexity int) int ID func(childComplexity int) int Identity func(childComplexity int) int @@ -1663,6 +1664,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Profile.CreatedAt(childComplexity), true + case "Profile.emailAddress": + if e.complexity.Profile.EmailAddress == nil { + break + } + + return e.complexity.Profile.EmailAddress(childComplexity), true case "Profile.fullName": if e.complexity.Profile.FullName == nil { break @@ -2708,6 +2715,7 @@ type Identity implements Node { type Profile implements Node { id: ID! fullName: String! + emailAddress: EmailAddr! source: String! state: ProfileState! additionalEmailAddresses: [EmailAddr!]! @@ -9003,6 +9011,8 @@ func (ec *executionContext) fieldContext_Organization_viewer(_ context.Context, return ec.fieldContext_Profile_id(ctx, field) case "fullName": return ec.fieldContext_Profile_fullName(ctx, field) + case "emailAddress": + return ec.fieldContext_Profile_emailAddress(ctx, field) case "source": return ec.fieldContext_Profile_source(ctx, field) case "state": @@ -9796,6 +9806,35 @@ func (ec *executionContext) fieldContext_Profile_fullName(_ context.Context, fie return fc, nil } +func (ec *executionContext) _Profile_emailAddress(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Profile_emailAddress, + func(ctx context.Context) (any, error) { + return obj.EmailAddress, nil + }, + nil, + ec.marshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Profile_emailAddress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Profile", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type EmailAddr does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Profile_source(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -10433,6 +10472,8 @@ func (ec *executionContext) fieldContext_ProfileEdge_node(_ context.Context, fie return ec.fieldContext_Profile_id(ctx, field) case "fullName": return ec.fieldContext_Profile_fullName(ctx, field) + case "emailAddress": + return ec.fieldContext_Profile_emailAddress(ctx, field) case "source": return ec.fieldContext_Profile_source(ctx, field) case "state": @@ -13907,6 +13948,8 @@ func (ec *executionContext) fieldContext_UpdateUserPayload_profile(_ context.Con return ec.fieldContext_Profile_id(ctx, field) case "fullName": return ec.fieldContext_Profile_fullName(ctx, field) + case "emailAddress": + return ec.fieldContext_Profile_emailAddress(ctx, field) case "source": return ec.fieldContext_Profile_source(ctx, field) case "state": @@ -19237,6 +19280,11 @@ func (ec *executionContext) _Profile(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "emailAddress": + out.Values[i] = ec._Profile_emailAddress(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "source": out.Values[i] = ec._Profile_source(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index 402bbd1ff..52e87fec0 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -310,6 +310,7 @@ type PersonalAPIKeyEdge struct { type Profile struct { ID gid.GID `json:"id"` FullName string `json:"fullName"` + EmailAddress mail.Addr `json:"emailAddress"` Source string `json:"source"` State coredata.ProfileState `json:"state"` AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses"` diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 118df3df8..42e72ace4 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -899,6 +899,7 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUse return nil, gqlutils.Conflictf(ctx, "user %q already exists", input.EmailAddress.String()) } + r.logger.ErrorCtx(ctx, "cannot create user", log.Error(err)) return nil, gqlutils.Internal(ctx) }