Reimplement remove / create in console

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-16 17:13:29 +04:00
parent f931bda10f
commit a42fc4fa61
26 changed files with 979 additions and 438 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<14b3e1990be5f8333046dffcada56516>>
* @generated SignedSource<<dd59cf57f55eff8458d1481b25e38991>>
* @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<string>;
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;

View File

@@ -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<string>;
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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<d29dfbdd0293d60af6d3f72e09dd5e9d>>
* @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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<7f025767ad277b8b8de6cbe9b1490312>>
* @generated SignedSource<<a9d41020a1c8eb8c673aa59fa2772468>>
* @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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6ee89b2ab0c55ac9498bf9186e78ee90>>
* @generated SignedSource<<a8e224db1200fa5aaa6edc17c09e7a35>>
* @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;

View File

@@ -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<string> | 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<string>;
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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<d1d8e96af7def93da24f32d96697106d>>
* @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<string> | 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<DataID>(
ConnectionHandler.getConnectionID(
organizationId,
"PeopleListFragment_profiles",
{ orderBy: { direction: "ASC", field: "CREATED_AT" } },
),
);
const { organization } = usePreloadedQuery<PeoplePageQuery>(
peoplePageQuery,
@@ -73,6 +82,12 @@ export function PeoplePage(props: {
return (
<div className="space-y-6">
<PageHeader title={__("People")}>
{organization.canCreateUser
&& (
<AddPersonDialog connectionId={connectionId}>
<Button variant="secondary">{__("Add Person")}</Button>
</AddPersonDialog>
)}
{organization.canInviteUser && (
<InviteUserDialog
connectionId={invitationListConnectionId}
@@ -105,7 +120,12 @@ export function PeoplePage(props: {
</Tabs>
<div className="pb-6 pt-6">
{activeTab === "people" && <PeopleList fKey={organization} />}
{activeTab === "people" && (
<PeopleList
fKey={organization}
onConnectionIdChange={setConnectionId}
/>
)}
{activeTab === "invitations" && (
<InvitationList

View File

@@ -1,6 +1,6 @@
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { ActionDropdown, Avatar, Breadcrumb, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui";
import { ActionDropdown, Avatar, Breadcrumb, Card, DropdownItem, IconTrashCan, useConfirm } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { useNavigate } from "react-router";
import { graphql } from "relay-runtime";
@@ -9,17 +9,15 @@ import type { PersonPageQuery } from "#/__generated__/iam/PersonPageQuery.graphq
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { PersonForm } from "./_components/PersonForm";
import { PersonFormLoader } from "./_components/PersonForm";
export const personPageQuery = graphql`
query PersonPageQuery($personId: ID!) {
person: node(id: $personId) @required(action: THROW) {
__typename
... on Profile {
id
fullName
membership @required(action: THROW) {
id
}
identity @required(action: THROW) {
email
}
@@ -30,12 +28,12 @@ export const personPageQuery = graphql`
}
`;
const removeMemberMutation = graphql`
const removeUserMutation = graphql`
mutation PersonPage_removeMutation(
$input: RemoveMemberInput!
$input: RemoveUserInput!
) {
removeMember(input: $input) {
deletedMembershipId
removeUser(input: $input) {
deletedProfileId
}
}
`;
@@ -53,21 +51,21 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
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<PersonPageQuery> })
)}
</div>
<PersonForm fragmentRef={person} />
<Card padded className="space-y-4">
<PersonFormLoader fragmentRef={person} />
</Card>
</div>
);
};

View File

@@ -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 (
<Dialog
title={__("Add Person")}
trigger={children}
className="max-w-xl"
>
<div className="p-4">
<PersonForm connectionId={connectionId} />
</div>
</Dialog>
);
}

View File

@@ -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

View File

@@ -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 (
<SortableTable
{...peoplePagination}
@@ -67,11 +82,11 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
>
<Thead>
<Tr>
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
<SortableTh field="EMAIL_ADDRESS">{__("Email")}</SortableTh>
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{__("Name")}</SortableTh>
<SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{__("Email")}</SortableTh>
<Th>{__("Type")}</Th>
<SortableTh field="ROLE">{__("Role")}</SortableTh>
<SortableTh field="CREATED_AT">{__("Joined")}</SortableTh>
<SortableTh field="ROLE" onOrderChange={handleOrderChange}>{__("Role")}</SortableTh>
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{__("Joined")}</SortableTh>
<Th>{__("Position")}</Th>
<Th></Th>
</Tr>
@@ -87,7 +102,7 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
)
: (
peoplePagination.data.profiles.edges.map(({ node: profile }) => (
<MemberListItem
<PeopleListItem
connectionId={peoplePagination.data.profiles.__id}
key={profile.id}
fKey={profile}

View File

@@ -1,25 +1,25 @@
import { sprintf } from "@probo/helpers";
import { getAssignableRoles, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Badge,
Button,
IconPencil,
IconTrashCan,
Option,
Select,
Spinner,
Td,
Tr,
useConfirm,
} from "@probo/ui";
import { clsx } from "clsx";
import { useState } from "react";
import { use } from "react";
import { useFragment } from "react-relay";
import { type DataID, graphql } from "relay-runtime";
import type { PeopleListItemFragment$key } from "#/__generated__/iam/PeopleListItemFragment.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { EditMemberDialog } from "../../settings/_components/EditMemberDialog";
import { CurrentUser } from "#/providers/CurrentUser";
const fragment = graphql`
fragment PeopleListItemFragment on Profile {
@@ -43,18 +43,29 @@ const fragment = graphql`
}
`;
const removeMemberMutation = graphql`
mutation PeopleListItem_removeMutation(
$input: RemoveMemberInput!
$connections: [ID!]!
) {
removeMember(input: $input) {
deletedMembershipId @deleteEdge(connections: $connections)
const updateRoleMutation = graphql`
mutation PeopleListItem_updateRoleMutation($input: UpdateMembershipInput!) {
updateMembership(input: $input) {
membership {
id
role
}
}
}
`;
export function MemberListItem(props: {
const removeUserMutation = graphql`
mutation PeopleListItem_removeMutation(
$input: RemoveUserInput!
$connections: [ID!]!
) {
removeUser(input: $input) {
deletedProfileId @deleteEdge(connections: $connections)
}
}
`;
export function PeopleListItem(props: {
connectionId: DataID;
fKey: PeopleListItemFragment$key;
onRefetch: () => 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<PeopleListItemFragment$key>(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: {
</div>
</Td>
<Td>{profile.kind}</Td>
<Td>
<Badge>{profile.membership.role}</Badge>
<Td noLink className="pr-4">
<Select
disabled={!profile.membership.canUpdate || isUpdatingRole}
value={profile.membership.role}
onValueChange={role => void handleUpdateRole(role)}
>
{availableRoles.includes("OWNER") && (
<Option value="OWNER">{__("Owner")}</Option>
)}
{availableRoles.includes("ADMIN") && (
<Option value="ADMIN">{__("Admin")}</Option>
)}
{availableRoles.includes("VIEWER") && (
<Option value="VIEWER">{__("Viewer")}</Option>
)}
{availableRoles.includes("AUDITOR") && (
<Option value="AUDITOR">{__("Auditor")}</Option>
)}
{availableRoles.includes("EMPLOYEE") && (
<Option value="EMPLOYEE">{__("Employee")}</Option>
)}
</Select>
</Td>
<Td>{new Date(profile.createdAt).toLocaleDateString()}</Td>
<Td>{profile.position}</Td>
@@ -133,15 +185,6 @@ export function MemberListItem(props: {
className="flex gap-2 justify-end"
onClick={e => e.stopPropagation()}
>
{profile.membership.canUpdate && (
<Button
variant="secondary"
onClick={() => setDialogOpen(true)}
disabled={dialogOpen}
icon={IconPencil}
aria-label={__("Edit role")}
/>
)}
{isRemoving
? (
<Spinner size={16} />
@@ -161,13 +204,6 @@ export function MemberListItem(props: {
)}
</Td>
</Tr>
{dialogOpen && (
<EditMemberDialog
onClose={() => setDialogOpen(false)}
profile={profile}
/>
)}
</>
);
}

View File

@@ -1,21 +1,31 @@
import { formatDatetime, getRoles, peopleRoles } from "@probo/helpers";
import { formatDatetime, getAssignableRoles, getRoles, peopleRoles } from "@probo/helpers";
import { roles } from "@probo/helpers/src/roles";
import { useTranslate } from "@probo/i18n";
import { Button, Card, Field, Input, Option } from "@probo/ui";
import { Button, Field, Input, Option } from "@probo/ui";
import { use } from "react";
import { useWatch } from "react-hook-form";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import { type DataID, graphql } from "relay-runtime";
import { z } from "zod";
import type { PersonForm_createMutation } from "#/__generated__/iam/PersonForm_createMutation.graphql";
import type { PersonForm_updateMutation } from "#/__generated__/iam/PersonForm_updateMutation.graphql";
import type { PersonFormFragment$key } from "#/__generated__/iam/PersonFormFragment.graphql";
import type { PersonFormMutation } from "#/__generated__/iam/PersonFormMutation.graphql";
import { ControlledField } from "#/components/form/ControlledField";
import { EmailsField } from "#/components/form/EmailsField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CurrentUser } from "#/providers/CurrentUser";
const fragment = graphql`
fragment PersonFormFragment on Profile {
id
fullName
emailAddress
membership @required(action: THROW) {
role
}
kind
position
additionalEmailAddresses
@@ -25,9 +35,21 @@ const fragment = graphql`
}
`;
const createPersonMutation = graphql`
mutation PersonForm_createMutation($input: CreateUserInput! $connections: [ID!]!) {
createUser(input: $input) {
profileEdge @prependEdge(connections: $connections) {
node {
...PeopleListItemFragment
}
}
}
}
`;
const updatePersonMutation = graphql`
mutation PersonFormMutation($input: UpdateProfileInput!) {
updateProfile(input: $input) {
mutation PersonForm_updateMutation($input: UpdateUserInput!) {
updateUser(input: $input) {
profile {
id
}
@@ -37,7 +59,9 @@ const updatePersonMutation = graphql`
const schema = z.object({
fullName: z.string().min(1),
position: z.string().min(1).nullable(),
emailAddress: z.string().email(),
role: z.enum(roles),
position: z.string().min(1).optional().nullable(),
additionalEmailAddresses: z.preprocess(
// Empty additional emails are skipped
v => (v as string[]).filter(v => !!v),
@@ -48,35 +72,59 @@ const schema = z.object({
contractEndDate: z.string().optional().nullable(),
});
export function PersonForm(props: { fragmentRef: PersonFormFragment$key }) {
const { fragmentRef } = props;
export function PersonForm(props: {
id?: string;
connectionId?: DataID;
disabled?: boolean;
defaultValues?: z.infer<typeof schema>;
}) {
const {
id,
connectionId = "",
disabled = false,
defaultValues = {
fullName: "",
emailAddress: "",
role: "EMPLOYEE",
additionalEmailAddresses: [],
kind: "EMPLOYEE",
position: null,
contractStartDate: null,
contractEndDate: null,
},
} = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const person = useFragment<PersonFormFragment$key>(fragment, fragmentRef);
const { role } = use(CurrentUser);
const availableRoles = getAssignableRoles(role);
const { control, formState, handleSubmit, register, reset }
= useFormWithSchema(schema, {
defaultValues: {
kind: person.kind,
fullName: person.fullName,
position: person.position,
additionalEmailAddresses: [...person.additionalEmailAddresses],
contractStartDate: person.contractStartDate?.split("T")[0] || "",
contractEndDate: person.contractEndDate?.split("T")[0] || "",
},
});
const [mutate, isMutating] = useMutationWithToasts<PersonFormMutation>(
= useFormWithSchema(schema, { defaultValues });
const watchedRole = useWatch({
control,
name: "role",
defaultValue: "EMPLOYEE",
});
const [createPerson, isCreating] = useMutationWithToasts<PersonForm_createMutation>(
createPersonMutation,
{
successMessage: __("Person created successfully."),
errorMessage: __("Failed to create person"),
},
);
const [updatePerson, isUpdating] = useMutationWithToasts<PersonForm_updateMutation>(
updatePersonMutation,
{
successMessage: __("Member updated successfully."),
errorMessage: __("Failed to update member"),
successMessage: __("Person updated successfully."),
errorMessage: __("Failed to update person"),
},
);
const onSubmit = handleSubmit(async (data: z.infer<typeof schema>) => {
const input = {
id: person.id,
fullName: data.fullName,
emailAddress: data.emailAddress,
role: data.role,
additionalEmailAddresses: data.additionalEmailAddresses,
kind: data.kind,
position: data.position,
@@ -84,61 +132,151 @@ export function PersonForm(props: { fragmentRef: PersonFormFragment$key }) {
contractEndDate: formatDatetime(data.contractEndDate) ?? null,
};
await mutate({
variables: { input },
onCompleted: () => {
reset(data);
},
});
if (id) {
await updatePerson({
variables: { input: { ...input, id } },
onCompleted: () => {
reset(data);
},
});
} else {
await createPerson({
variables: {
input: { ...input, organizationId },
connections: [connectionId],
},
onCompleted: () => {
reset(data);
},
});
}
});
return (
<form onSubmit={e => void onSubmit(e)} className="space-y-4">
<Card padded className="space-y-4">
<Field label={__("Full name")} {...register("fullName")} type="text" />
<ControlledField
control={control}
name="kind"
type="select"
label={__("Type")}
disabled={!person.canUpdate}
>
{getRoles(__).map(role => (
<Option key={role.value} value={role.value}>
{role.label}
</Option>
))}
</ControlledField>
<Field
label={__("Position")}
{...register("position")}
type="text"
placeholder={__("e.g. CEO, CFO, etc.")}
disabled={!person.canUpdate}
<Field label={__("Full name *")} {...register("fullName")} type="text" />
{id
? (
<>
<input type="hidden" {...register("emailAddress")} disabled />
<input type="hidden" {...register("role")} disabled />
</>
)
: (
<>
<Field label={__("Email Address *")} {...register("emailAddress")} type="email" disabled={disabled || !!id} />
<ControlledField
control={control}
name="role"
type="select"
label={__("Role *")}
disabled={disabled || !!id}
>
{availableRoles.includes("OWNER") && (
<Option value="OWNER">{__("Owner")}</Option>
)}
{availableRoles.includes("ADMIN") && (
<Option value="ADMIN">{__("Admin")}</Option>
)}
{availableRoles.includes("VIEWER") && (
<Option value="VIEWER">{__("Viewer")}</Option>
)}
{availableRoles.includes("AUDITOR") && (
<Option value="AUDITOR">{__("Auditor")}</Option>
)}
{availableRoles.includes("EMPLOYEE") && (
<Option value="EMPLOYEE">{__("Employee")}</Option>
)}
</ControlledField>
<div className="mt-4 space-y-2 text-sm text-txt-tertiary">
{watchedRole === "OWNER" && (
<p>{__("Full access to everything")}</p>
)}
{watchedRole === "ADMIN" && (
<p>
{__("Full access except organization setup and API keys")}
</p>
)}
{watchedRole === "VIEWER" && <p>{__("Read-only access")}</p>}
{watchedRole === "AUDITOR" && (
<p>
{__("Read-only access without settings, tasks and meetings")}
</p>
)}
{watchedRole === "EMPLOYEE" && (
<p>{__("Access to employee page")}</p>
)}
</div>
</>
)}
<ControlledField
control={control}
name="kind"
type="select"
label={__("Type")}
disabled={disabled}
>
{getRoles(__).map(role => (
<Option key={role.value} value={role.value}>
{role.label}
</Option>
))}
</ControlledField>
<Field
label={__("Position")}
{...register("position")}
type="text"
placeholder={__("e.g. CEO, CFO, etc.")}
disabled={disabled}
/>
<EmailsField control={control} register={register} />
<Field label={__("Contract start date")}>
<Input
{...register("contractStartDate")}
type="date"
disabled={disabled}
/>
<EmailsField control={control} register={register} />
<Field label={__("Contract start date")}>
<Input
{...register("contractStartDate")}
type="date"
disabled={!person.canUpdate}
/>
</Field>
<Field label={__("Contract end date")}>
<Input
{...register("contractEndDate")}
type="date"
disabled={!person.canUpdate}
/>
</Field>
</Card>
</Field>
<Field label={__("Contract end date")}>
<Input
{...register("contractEndDate")}
type="date"
disabled={disabled}
/>
</Field>
<div className="flex justify-end">
{formState.isDirty && person.canUpdate && (
<Button type="submit" disabled={isMutating}>
{__("Update")}
{(!id || formState.isDirty) && !disabled && (
<Button type="submit" disabled={isUpdating || isCreating || !formState.isValid}>
{id ? __("Update") : __("Create")}
</Button>
)}
</div>
</form>
);
}
export function PersonFormLoader(props: { fragmentRef: PersonFormFragment$key }) {
const { fragmentRef } = props;
const person = useFragment<PersonFormFragment$key>(fragment, fragmentRef);
return (
<PersonForm
id={person.id}
disabled={!person.canUpdate}
defaultValues={
{
kind: person.kind,
fullName: person.fullName,
emailAddress: person.emailAddress,
role: person.membership.role,
position: person.position,
additionalEmailAddresses: [...person.additionalEmailAddresses],
contractStartDate: person.contractStartDate?.split("T")[0] || "",
contractEndDate: person.contractEndDate?.split("T")[0] || "",
}
}
/>
);
}

View File

@@ -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<string>(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 (
<Dialog defaultOpen={true} onClose={onClose} title={__("Edit Profile")}>
<form onSubmit={e => void handleUpdateRole(e)}>
<DialogContent padded className="space-y-6">
<div>
<p className="text-txt-secondary text-sm mb-4">
{sprintf(
__("Update the role for %s"),
profile.fullName,
)}
</p>
<Field label={__("Role")} required>
<Select value={selectedRole} onValueChange={setSelectedRole}>
{availableRoles.includes("OWNER") && (
<Option value="OWNER">{__("Owner")}</Option>
)}
{availableRoles.includes("ADMIN") && (
<Option value="ADMIN">{__("Admin")}</Option>
)}
{availableRoles.includes("VIEWER") && (
<Option value="VIEWER">{__("Viewer")}</Option>
)}
{availableRoles.includes("AUDITOR") && (
<Option value="AUDITOR">{__("Auditor")}</Option>
)}
{availableRoles.includes("EMPLOYEE") && (
<Option value="EMPLOYEE">{__("Employee")}</Option>
)}
</Select>
</Field>
<div className="mt-4 space-y-2 text-sm text-txt-tertiary">
{selectedRole === "OWNER" && (
<p>{__("Full access to everything")}</p>
)}
{selectedRole === "ADMIN" && (
<p>
{__("Full access except organization setup and API keys")}
</p>
)}
{selectedRole === "VIEWER" && <p>{__("Read-only access")}</p>}
{selectedRole === "AUDITOR" && (
<p>
{__("Read-only access without settings, tasks and meetings")}
</p>
)}
{selectedRole === "EMPLOYEE" && (
<p>{__("Access to employee page")}</p>
)}
</div>
</div>
</DialogContent>
<DialogFooter>
<Button
type="submit"
disabled={isUpdating || selectedRole === profile.membership.role}
>
{isUpdating && <Spinner />}
{__("Update Role")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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

View File

@@ -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")),

View File

@@ -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,

View File

@@ -183,6 +183,7 @@ type Identity implements Node {
type Profile implements Node {
id: ID!
fullName: String!
emailAddress: EmailAddr!
source: String!
state: ProfileState!
additionalEmailAddresses: [EmailAddr!]!

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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)
}