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 * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,18 +10,16 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type InviteMemberInput = { export type InviteUserInput = {
email: string;
fullName: string;
organizationId: string; organizationId: string;
role: MembershipRole; profileId: string;
}; };
export type InviteUserDialogMutation$variables = { export type InviteUserDialogMutation$variables = {
connections: ReadonlyArray<string>; connections: ReadonlyArray<string>;
input: InviteMemberInput; input: InviteUserInput;
}; };
export type InviteUserDialogMutation$data = { export type InviteUserDialogMutation$data = {
readonly inviteMember: { readonly inviteUser: {
readonly invitationEdge: { readonly invitationEdge: {
readonly node: { readonly node: {
readonly acceptedAt: string | null | undefined; readonly acceptedAt: string | null | undefined;
@@ -156,9 +154,9 @@ return {
{ {
"alias": null, "alias": null,
"args": (v2/*: any*/), "args": (v2/*: any*/),
"concreteType": "InviteMemberPayload", "concreteType": "InviteUserPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "inviteMember", "name": "inviteUser",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/) (v3/*: any*/)
@@ -181,9 +179,9 @@ return {
{ {
"alias": null, "alias": null,
"args": (v2/*: any*/), "args": (v2/*: any*/),
"concreteType": "InviteMemberPayload", "concreteType": "InviteUserPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "inviteMember", "name": "inviteUser",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
@@ -209,16 +207,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "8d9e447c08739bef2460474225fb77bf", "cacheID": "685e0bc12871b33065d1fe03334110b6",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "InviteUserDialogMutation", "name": "InviteUserDialogMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<8c0cc8f8d9d0418ea1d817412793f2dc>> * @generated SignedSource<<921243e6b4b9007688051434ab212f85>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,17 +9,17 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type RemoveMemberInput = { export type RemoveUserInput = {
membershipId: string;
organizationId: string; organizationId: string;
profileId: string;
}; };
export type PeopleListItem_removeMutation$variables = { export type PeopleListItem_removeMutation$variables = {
connections: ReadonlyArray<string>; connections: ReadonlyArray<string>;
input: RemoveMemberInput; input: RemoveUserInput;
}; };
export type PeopleListItem_removeMutation$data = { export type PeopleListItem_removeMutation$data = {
readonly removeMember: { readonly removeUser: {
readonly deletedMembershipId: string; readonly deletedProfileId: string;
} | null | undefined; } | null | undefined;
}; };
export type PeopleListItem_removeMutation = { export type PeopleListItem_removeMutation = {
@@ -49,7 +49,7 @@ v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "deletedMembershipId", "name": "deletedProfileId",
"storageKey": null "storageKey": null
}; };
return { return {
@@ -65,9 +65,9 @@ return {
{ {
"alias": null, "alias": null,
"args": (v2/*: any*/), "args": (v2/*: any*/),
"concreteType": "RemoveMemberPayload", "concreteType": "RemoveUserPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "removeMember", "name": "removeUser",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/) (v3/*: any*/)
@@ -90,9 +90,9 @@ return {
{ {
"alias": null, "alias": null,
"args": (v2/*: any*/), "args": (v2/*: any*/),
"concreteType": "RemoveMemberPayload", "concreteType": "RemoveUserPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "removeMember", "name": "removeUser",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
@@ -103,7 +103,7 @@ return {
"handle": "deleteEdge", "handle": "deleteEdge",
"key": "", "key": "",
"kind": "ScalarHandle", "kind": "ScalarHandle",
"name": "deletedMembershipId", "name": "deletedProfileId",
"handleArgs": [ "handleArgs": [
{ {
"kind": "Variable", "kind": "Variable",
@@ -118,16 +118,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "d5a0e195ee72906380f566bcecb45a3f", "cacheID": "ec2aeab79c24169e2f8fc95ca3b28a7d",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PeopleListItem_removeMutation", "name": "PeopleListItem_removeMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<d29dfbdd0293d60af6d3f72e09dd5e9d>> * @generated SignedSource<<021e2d930ac4fe38c64cede962db93cf>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -15,10 +15,10 @@ export type UpdateMembershipInput = {
organizationId: string; organizationId: string;
role: MembershipRole; role: MembershipRole;
}; };
export type EditMemberDialog_updateMutation$variables = { export type PeopleListItem_updateRoleMutation$variables = {
input: UpdateMembershipInput; input: UpdateMembershipInput;
}; };
export type EditMemberDialog_updateMutation$data = { export type PeopleListItem_updateRoleMutation$data = {
readonly updateMembership: { readonly updateMembership: {
readonly membership: { readonly membership: {
readonly id: string; readonly id: string;
@@ -26,9 +26,9 @@ export type EditMemberDialog_updateMutation$data = {
}; };
}; };
}; };
export type EditMemberDialog_updateMutation = { export type PeopleListItem_updateRoleMutation = {
response: EditMemberDialog_updateMutation$data; response: PeopleListItem_updateRoleMutation$data;
variables: EditMemberDialog_updateMutation$variables; variables: PeopleListItem_updateRoleMutation$variables;
}; };
const node: ConcreteRequest = (function(){ const node: ConcreteRequest = (function(){
@@ -88,7 +88,7 @@ return {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "EditMemberDialog_updateMutation", "name": "PeopleListItem_updateRoleMutation",
"selections": (v1/*: any*/), "selections": (v1/*: any*/),
"type": "Mutation", "type": "Mutation",
"abstractKey": null "abstractKey": null
@@ -97,20 +97,20 @@ return {
"operation": { "operation": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
"kind": "Operation", "kind": "Operation",
"name": "EditMemberDialog_updateMutation", "name": "PeopleListItem_updateRoleMutation",
"selections": (v1/*: any*/) "selections": (v1/*: any*/)
}, },
"params": { "params": {
"cacheID": "696ec9f74220b92171bf60630e5c8d7f", "cacheID": "d15bdc1ff79729c3878c0cef9095a241",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "EditMemberDialog_updateMutation", "name": "PeopleListItem_updateRoleMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<7f025767ad277b8b8de6cbe9b1490312>> * @generated SignedSource<<a9d41020a1c8eb8c673aa59fa2772468>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,6 +16,7 @@ export type PeoplePageQuery$variables = {
export type PeoplePageQuery$data = { export type PeoplePageQuery$data = {
readonly organization: { readonly organization: {
readonly __typename: "Organization"; readonly __typename: "Organization";
readonly canCreateUser: boolean;
readonly canInviteUser: boolean; readonly canInviteUser: boolean;
readonly invitations: { readonly invitations: {
readonly $updatableFragmentSpreads: FragmentRefs<"PeoplePage_invitationsTotalCountFragment">; readonly $updatableFragmentSpreads: FragmentRefs<"PeoplePage_invitationsTotalCountFragment">;
@@ -59,6 +60,19 @@ v2 = {
"storageKey": null "storageKey": null
}, },
v3 = { 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", "alias": "canInviteUser",
"args": [ "args": [
{ {
@@ -71,85 +85,85 @@ v3 = {
"name": "permission", "name": "permission",
"storageKey": "permission(action:\"iam:invitation:create\")" "storageKey": "permission(action:\"iam:invitation:create\")"
}, },
v4 = { v5 = {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 20 "value": 20
}, },
v5 = { v6 = {
"direction": "ASC", "direction": "ASC",
"field": "FULL_NAME" "field": "FULL_NAME"
}, },
v6 = [ v7 = [
(v4/*: any*/), (v5/*: any*/),
{ {
"kind": "Literal", "kind": "Literal",
"name": "orderBy", "name": "orderBy",
"value": (v5/*: any*/) "value": (v6/*: any*/)
} }
], ],
v7 = { v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "totalCount", "name": "totalCount",
"storageKey": null "storageKey": null
}, },
v8 = { v9 = {
"direction": "DESC", "direction": "DESC",
"field": "CREATED_AT" "field": "CREATED_AT"
}, },
v9 = [ v10 = [
(v4/*: any*/), (v5/*: any*/),
{ {
"kind": "Literal", "kind": "Literal",
"name": "orderBy", "name": "orderBy",
"value": (v8/*: any*/) "value": (v9/*: any*/)
} }
], ],
v10 = { v11 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "id",
"storageKey": null "storageKey": null
}, },
v11 = { v12 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
v12 = { v13 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "role", "name": "role",
"storageKey": null "storageKey": null
}, },
v13 = { v14 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "email", "name": "email",
"storageKey": null "storageKey": null
}, },
v14 = { v15 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
v15 = { v16 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "cursor",
"storageKey": null "storageKey": null
}, },
v16 = { v17 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -188,7 +202,7 @@ v16 = {
], ],
"storageKey": null "storageKey": null
}, },
v17 = { v18 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -200,7 +214,7 @@ v17 = {
} }
] ]
}, },
v18 = [ v19 = [
"orderBy" "orderBy"
]; ];
return { return {
@@ -225,17 +239,18 @@ return {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/),
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
"alias": null, "alias": null,
"args": (v6/*: any*/), "args": (v7/*: any*/),
"concreteType": "ProfileConnection", "concreteType": "ProfileConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "profiles", "name": "profiles",
"plural": false, "plural": false,
"selections": [ "selections": [
(v7/*: any*/) (v8/*: any*/)
], ],
"storageKey": "profiles(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})" "storageKey": "profiles(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
}, },
@@ -243,11 +258,11 @@ return {
}, },
{ {
"args": [ "args": [
(v4/*: any*/), (v5/*: any*/),
{ {
"kind": "Literal", "kind": "Literal",
"name": "order", "name": "order",
"value": (v5/*: any*/) "value": (v6/*: any*/)
} }
], ],
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -257,13 +272,13 @@ return {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
"alias": null, "alias": null,
"args": (v9/*: any*/), "args": (v10/*: any*/),
"concreteType": "InvitationConnection", "concreteType": "InvitationConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "invitations", "name": "invitations",
"plural": false, "plural": false,
"selections": [ "selections": [
(v7/*: any*/), (v8/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -276,11 +291,11 @@ return {
}, },
{ {
"args": [ "args": [
(v4/*: any*/), (v5/*: any*/),
{ {
"kind": "Literal", "kind": "Literal",
"name": "order", "name": "order",
"value": (v8/*: any*/) "value": (v9/*: any*/)
} }
], ],
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -314,20 +329,21 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
(v10/*: any*/), (v11/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v6/*: any*/), "args": (v7/*: any*/),
"concreteType": "ProfileConnection", "concreteType": "ProfileConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "profiles", "name": "profiles",
"plural": false, "plural": false,
"selections": [ "selections": [
(v7/*: any*/), (v8/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -344,7 +360,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v10/*: any*/), (v11/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -359,7 +375,7 @@ return {
"name": "state", "name": "state",
"storageKey": null "storageKey": null
}, },
(v11/*: any*/), (v12/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -382,8 +398,8 @@ return {
"name": "membership", "name": "membership",
"plural": false, "plural": false,
"selections": [ "selections": [
(v10/*: any*/), (v11/*: any*/),
(v12/*: any*/), (v13/*: any*/),
{ {
"alias": "canUpdate", "alias": "canUpdate",
"args": [ "args": [
@@ -421,12 +437,12 @@ return {
"name": "identity", "name": "identity",
"plural": false, "plural": false,
"selections": [ "selections": [
(v13/*: any*/), (v14/*: any*/),
(v10/*: any*/) (v11/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v14/*: any*/), (v15/*: any*/),
{ {
"alias": "canUpdate", "alias": "canUpdate",
"args": [ "args": [
@@ -444,19 +460,19 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v16/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v16/*: any*/), (v17/*: any*/),
(v17/*: any*/) (v18/*: any*/)
], ],
"storageKey": "profiles(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})" "storageKey": "profiles(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
}, },
{ {
"alias": null, "alias": null,
"args": (v6/*: any*/), "args": (v7/*: any*/),
"filters": (v18/*: any*/), "filters": (v19/*: any*/),
"handle": "connection", "handle": "connection",
"key": "PeopleListFragment_profiles", "key": "PeopleListFragment_profiles",
"kind": "LinkedHandle", "kind": "LinkedHandle",
@@ -464,13 +480,13 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v9/*: any*/), "args": (v10/*: any*/),
"concreteType": "InvitationConnection", "concreteType": "InvitationConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "invitations", "name": "invitations",
"plural": false, "plural": false,
"selections": [ "selections": [
(v7/*: any*/), (v8/*: any*/),
(v2/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
@@ -488,10 +504,10 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v10/*: any*/),
(v11/*: any*/), (v11/*: any*/),
(v13/*: any*/),
(v12/*: any*/), (v12/*: any*/),
(v14/*: any*/),
(v13/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -499,7 +515,7 @@ return {
"name": "status", "name": "status",
"storageKey": null "storageKey": null
}, },
(v14/*: any*/), (v15/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -524,19 +540,19 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v16/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v16/*: any*/), (v17/*: any*/),
(v17/*: any*/) (v18/*: any*/)
], ],
"storageKey": "invitations(first:20,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" "storageKey": "invitations(first:20,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
}, },
{ {
"alias": null, "alias": null,
"args": (v9/*: any*/), "args": (v10/*: any*/),
"filters": (v18/*: any*/), "filters": (v19/*: any*/),
"handle": "connection", "handle": "connection",
"key": "InvitationListFragment_invitations", "key": "InvitationListFragment_invitations",
"kind": "LinkedHandle", "kind": "LinkedHandle",
@@ -552,16 +568,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "b67de1f40d556a5e4a6d78104e2eb224", "cacheID": "3aef3e0e345bd04d2432c7167c87aa8d",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PeoplePageQuery", "name": "PeoplePageQuery",
"operationKind": "query", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6ee89b2ab0c55ac9498bf9186e78ee90>> * @generated SignedSource<<a8e224db1200fa5aaa6edc17c09e7a35>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,6 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type PersonFormFragment$data = { export type PersonFormFragment$data = {
@@ -16,9 +17,13 @@ export type PersonFormFragment$data = {
readonly canUpdate: boolean; readonly canUpdate: boolean;
readonly contractEndDate: string | null | undefined; readonly contractEndDate: string | null | undefined;
readonly contractStartDate: string | null | undefined; readonly contractStartDate: string | null | undefined;
readonly emailAddress: string;
readonly fullName: string; readonly fullName: string;
readonly id: string; readonly id: string;
readonly kind: ProfileKind; readonly kind: ProfileKind;
readonly membership: {
readonly role: MembershipRole;
};
readonly position: string | null | undefined; readonly position: string | null | undefined;
readonly " $fragmentType": "PersonFormFragment"; readonly " $fragmentType": "PersonFormFragment";
}; };
@@ -47,6 +52,35 @@ const node: ReaderFragment = {
"name": "fullName", "name": "fullName",
"storageKey": null "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, "alias": null,
"args": null, "args": null,
@@ -100,6 +134,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "9ecc41c74271a3202bbde08b63c92b73"; (node as any).hash = "4f99d9b9ed708290c5a7d7ec258a0b1d";
export default node; 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 * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,7 +10,7 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT"; export type ProfileKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
export type UpdateProfileInput = { export type UpdateUserInput = {
additionalEmailAddresses?: ReadonlyArray<string> | null | undefined; additionalEmailAddresses?: ReadonlyArray<string> | null | undefined;
contractEndDate?: string | null | undefined; contractEndDate?: string | null | undefined;
contractStartDate?: string | null | undefined; contractStartDate?: string | null | undefined;
@@ -19,19 +19,19 @@ export type UpdateProfileInput = {
kind: ProfileKind; kind: ProfileKind;
position?: string | null | undefined; position?: string | null | undefined;
}; };
export type PersonFormMutation$variables = { export type PersonForm_updateMutation$variables = {
input: UpdateProfileInput; input: UpdateUserInput;
}; };
export type PersonFormMutation$data = { export type PersonForm_updateMutation$data = {
readonly updateProfile: { readonly updateUser: {
readonly profile: { readonly profile: {
readonly id: string; readonly id: string;
}; };
}; };
}; };
export type PersonFormMutation = { export type PersonForm_updateMutation = {
response: PersonFormMutation$data; response: PersonForm_updateMutation$data;
variables: PersonFormMutation$variables; variables: PersonForm_updateMutation$variables;
}; };
const node: ConcreteRequest = (function(){ const node: ConcreteRequest = (function(){
@@ -52,9 +52,9 @@ v1 = [
"variableName": "input" "variableName": "input"
} }
], ],
"concreteType": "UpdateProfilePayload", "concreteType": "UpdateUserPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "updateProfile", "name": "updateUser",
"plural": false, "plural": false,
"selections": [ "selections": [
{ {
@@ -84,7 +84,7 @@ return {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "PersonFormMutation", "name": "PersonForm_updateMutation",
"selections": (v1/*: any*/), "selections": (v1/*: any*/),
"type": "Mutation", "type": "Mutation",
"abstractKey": null "abstractKey": null
@@ -93,20 +93,20 @@ return {
"operation": { "operation": {
"argumentDefinitions": (v0/*: any*/), "argumentDefinitions": (v0/*: any*/),
"kind": "Operation", "kind": "Operation",
"name": "PersonFormMutation", "name": "PersonForm_updateMutation",
"selections": (v1/*: any*/) "selections": (v1/*: any*/)
}, },
"params": { "params": {
"cacheID": "fe068a78bed066f0d50cab1dde6db292", "cacheID": "0623be6bdbd6575b04173a59b1dd45f7",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PersonFormMutation", "name": "PersonForm_updateMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<2e75f19fcdaf2b78a187c33458c9cfe0>> * @generated SignedSource<<679cd9698980b9fdf8b330df8df3aac5>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -18,12 +18,10 @@ export type PersonPageQuery$data = {
readonly __typename: "Profile"; readonly __typename: "Profile";
readonly canDelete: boolean; readonly canDelete: boolean;
readonly fullName: string; readonly fullName: string;
readonly id: string;
readonly identity: { readonly identity: {
readonly email: string; readonly email: string;
}; };
readonly membership: {
readonly id: string;
};
readonly " $fragmentSpreads": FragmentRefs<"PersonFormFragment">; readonly " $fragmentSpreads": FragmentRefs<"PersonFormFragment">;
} | { } | {
// This will never be '%other', but we need some // This will never be '%other', but we need some
@@ -62,36 +60,24 @@ v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fullName", "name": "id",
"storageKey": null "storageKey": null
}, },
v4 = { v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "fullName",
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
(v4/*: any*/)
],
"storageKey": null
},
v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "email", "name": "email",
"storageKey": null "storageKey": null
}, },
v7 = { v6 = {
"alias": "canDelete", "alias": "canDelete",
"args": [ "args": [
{ {
@@ -126,11 +112,7 @@ return {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
{ (v4/*: any*/),
"kind": "RequiredField",
"field": (v5/*: any*/),
"action": "THROW"
},
{ {
"kind": "RequiredField", "kind": "RequiredField",
"field": { "field": {
@@ -141,13 +123,13 @@ return {
"name": "identity", "name": "identity",
"plural": false, "plural": false,
"selections": [ "selections": [
(v6/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
"action": "THROW" "action": "THROW"
}, },
(v7/*: any*/), (v6/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -181,12 +163,11 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
(v4/*: any*/), (v3/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v3/*: any*/), (v4/*: any*/),
(v5/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -195,12 +176,38 @@ return {
"name": "identity", "name": "identity",
"plural": false, "plural": false,
"selections": [ "selections": [
(v6/*: any*/), (v5/*: any*/),
(v4/*: 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 "storageKey": null
}, },
(v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -259,16 +266,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "e148eb605cc90b57c6c7772b9045217d", "cacheID": "cb1cec2fd7c66b42f61d40e6a22b0d67",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PersonPageQuery", "name": "PersonPageQuery",
"operationKind": "query", "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; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<0c55769185354921be98596538492fab>> * @generated SignedSource<<7df320d2cbda9f48737622295ed141ad>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,16 +9,16 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type RemoveMemberInput = { export type RemoveUserInput = {
membershipId: string;
organizationId: string; organizationId: string;
profileId: string;
}; };
export type PersonPage_removeMutation$variables = { export type PersonPage_removeMutation$variables = {
input: RemoveMemberInput; input: RemoveUserInput;
}; };
export type PersonPage_removeMutation$data = { export type PersonPage_removeMutation$data = {
readonly removeMember: { readonly removeUser: {
readonly deletedMembershipId: string; readonly deletedProfileId: string;
} | null | undefined; } | null | undefined;
}; };
export type PersonPage_removeMutation = { export type PersonPage_removeMutation = {
@@ -44,16 +44,16 @@ v1 = [
"variableName": "input" "variableName": "input"
} }
], ],
"concreteType": "RemoveMemberPayload", "concreteType": "RemoveUserPayload",
"kind": "LinkedField", "kind": "LinkedField",
"name": "removeMember", "name": "removeUser",
"plural": false, "plural": false,
"selections": [ "selections": [
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "deletedMembershipId", "name": "deletedProfileId",
"storageKey": null "storageKey": null
} }
], ],
@@ -78,16 +78,16 @@ return {
"selections": (v1/*: any*/) "selections": (v1/*: any*/)
}, },
"params": { "params": {
"cacheID": "0c220926bf64fda6954b3ab8e23dcd72", "cacheID": "1e686e9dcb1425cde221721b4635a747",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PersonPage_removeMutation", "name": "PersonPage_removeMutation",
"operationKind": "mutation", "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; export default node;

View File

@@ -2,11 +2,12 @@ import { useTranslate } from "@probo/i18n";
import { Button, PageHeader, TabBadge, TabItem, Tabs } from "@probo/ui"; import { Button, PageHeader, TabBadge, TabItem, Tabs } from "@probo/ui";
import { useState } from "react"; import { useState } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; 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 type { PeoplePageQuery } from "#/__generated__/iam/PeoplePageQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { AddPersonDialog } from "./_components/AddPersonDialog";
import { InvitationList } from "./_components/InvitationList"; import { InvitationList } from "./_components/InvitationList";
import { InviteUserDialog } from "./_components/InviteUserDialog"; import { InviteUserDialog } from "./_components/InviteUserDialog";
import { PeopleList } from "./_components/PeopleList"; import { PeopleList } from "./_components/PeopleList";
@@ -16,6 +17,7 @@ export const peoplePageQuery = graphql`
organization: node(id: $organizationId) @required(action: THROW) { organization: node(id: $organizationId) @required(action: THROW) {
__typename __typename
... on Organization { ... on Organization {
canCreateUser: permission(action: "iam:membership-profile:create")
canInviteUser: permission(action: "iam:invitation:create") canInviteUser: permission(action: "iam:invitation:create")
profiles(first: 20, orderBy: { direction: ASC, field: FULL_NAME }) profiles(first: 20, orderBy: { direction: ASC, field: FULL_NAME })
@required(action: THROW) { @required(action: THROW) {
@@ -53,6 +55,13 @@ export function PeoplePage(props: {
const [activeTab, setActiveTab] = useState<"people" | "invitations">( const [activeTab, setActiveTab] = useState<"people" | "invitations">(
"people", "people",
); );
const [connectionId, setConnectionId] = useState<DataID>(
ConnectionHandler.getConnectionID(
organizationId,
"PeopleListFragment_profiles",
{ orderBy: { direction: "ASC", field: "CREATED_AT" } },
),
);
const { organization } = usePreloadedQuery<PeoplePageQuery>( const { organization } = usePreloadedQuery<PeoplePageQuery>(
peoplePageQuery, peoplePageQuery,
@@ -73,6 +82,12 @@ export function PeoplePage(props: {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader title={__("People")}> <PageHeader title={__("People")}>
{organization.canCreateUser
&& (
<AddPersonDialog connectionId={connectionId}>
<Button variant="secondary">{__("Add Person")}</Button>
</AddPersonDialog>
)}
{organization.canInviteUser && ( {organization.canInviteUser && (
<InviteUserDialog <InviteUserDialog
connectionId={invitationListConnectionId} connectionId={invitationListConnectionId}
@@ -105,7 +120,12 @@ export function PeoplePage(props: {
</Tabs> </Tabs>
<div className="pb-6 pt-6"> <div className="pb-6 pt-6">
{activeTab === "people" && <PeopleList fKey={organization} />} {activeTab === "people" && (
<PeopleList
fKey={organization}
onConnectionIdChange={setConnectionId}
/>
)}
{activeTab === "invitations" && ( {activeTab === "invitations" && (
<InvitationList <InvitationList

View File

@@ -1,6 +1,6 @@
import { sprintf } from "@probo/helpers"; import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; 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 { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -9,17 +9,15 @@ import type { PersonPageQuery } from "#/__generated__/iam/PersonPageQuery.graphq
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { PersonForm } from "./_components/PersonForm"; import { PersonFormLoader } from "./_components/PersonForm";
export const personPageQuery = graphql` export const personPageQuery = graphql`
query PersonPageQuery($personId: ID!) { query PersonPageQuery($personId: ID!) {
person: node(id: $personId) @required(action: THROW) { person: node(id: $personId) @required(action: THROW) {
__typename __typename
... on Profile { ... on Profile {
fullName
membership @required(action: THROW) {
id id
} fullName
identity @required(action: THROW) { identity @required(action: THROW) {
email email
} }
@@ -30,12 +28,12 @@ export const personPageQuery = graphql`
} }
`; `;
const removeMemberMutation = graphql` const removeUserMutation = graphql`
mutation PersonPage_removeMutation( mutation PersonPage_removeMutation(
$input: RemoveMemberInput! $input: RemoveUserInput!
) { ) {
removeMember(input: $input) { removeUser(input: $input) {
deletedMembershipId deletedProfileId
} }
} }
`; `;
@@ -53,21 +51,21 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
throw new Error("invalid type for node"); throw new Error("invalid type for node");
} }
const [removeMembership, isRemoving] = useMutationWithToasts( const [removeUser, isRemoving] = useMutationWithToasts(
removeMemberMutation, removeUserMutation,
{ {
successMessage: __("Member removed successfully"), successMessage: __("Person removed successfully"),
errorMessage: __("Failed to remove member"), errorMessage: __("Failed to remove person"),
}, },
); );
const handleRemove = () => { const handleRemove = () => {
confirm( confirm(
() => { () => {
return removeMembership({ return removeUser({
variables: { variables: {
input: { input: {
membershipId: person.membership.id, profileId: person.id,
organizationId: organizationId, organizationId: organizationId,
}, },
}, },
@@ -121,7 +119,9 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
)} )}
</div> </div>
<PersonForm fragmentRef={person} /> <Card padded className="space-y-4">
<PersonFormLoader fragmentRef={person} />
</Card>
</div> </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` const inviteMutation = graphql`
mutation InviteUserDialogMutation( mutation InviteUserDialogMutation(
$input: InviteMemberInput! $input: InviteUserInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
inviteMember(input: $input) { inviteUser(input: $input) {
invitationEdge @prependEdge(connections: $connections) { invitationEdge @prependEdge(connections: $connections) {
node { node {
id id

View File

@@ -1,13 +1,14 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui"; import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import type { ComponentProps } from "react"; 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$key } from "#/__generated__/iam/PeopleListFragment.graphql";
import type { PeopleListFragment_RefetchQuery } from "#/__generated__/iam/PeopleListFragment_RefetchQuery.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` const fragment = graphql`
fragment PeopleListFragment on Organization fragment PeopleListFragment on Organization
@@ -41,9 +42,13 @@ const fragment = graphql`
} }
`; `;
export function PeopleList(props: { fKey: PeopleListFragment$key }) { export function PeopleList(props: {
const { fKey } = props; onConnectionIdChange: (connectionId: string) => void;
fKey: PeopleListFragment$key;
}) {
const { fKey, onConnectionIdChange } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const peoplePagination = usePaginationFragment< const peoplePagination = usePaginationFragment<
@@ -55,6 +60,16 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
peoplePagination.refetch({}, { fetchPolicy: "network-only" }); peoplePagination.refetch({}, { fetchPolicy: "network-only" });
}; };
const handleOrderChange = (order: Order) => {
onConnectionIdChange(
ConnectionHandler.getConnectionID(
organizationId,
"PeopleListFragment_profiles",
{ orderBy: order },
),
);
};
return ( return (
<SortableTable <SortableTable
{...peoplePagination} {...peoplePagination}
@@ -67,11 +82,11 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
> >
<Thead> <Thead>
<Tr> <Tr>
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh> <SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{__("Name")}</SortableTh>
<SortableTh field="EMAIL_ADDRESS">{__("Email")}</SortableTh> <SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{__("Email")}</SortableTh>
<Th>{__("Type")}</Th> <Th>{__("Type")}</Th>
<SortableTh field="ROLE">{__("Role")}</SortableTh> <SortableTh field="ROLE" onOrderChange={handleOrderChange}>{__("Role")}</SortableTh>
<SortableTh field="CREATED_AT">{__("Joined")}</SortableTh> <SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{__("Joined")}</SortableTh>
<Th>{__("Position")}</Th> <Th>{__("Position")}</Th>
<Th></Th> <Th></Th>
</Tr> </Tr>
@@ -87,7 +102,7 @@ export function PeopleList(props: { fKey: PeopleListFragment$key }) {
) )
: ( : (
peoplePagination.data.profiles.edges.map(({ node: profile }) => ( peoplePagination.data.profiles.edges.map(({ node: profile }) => (
<MemberListItem <PeopleListItem
connectionId={peoplePagination.data.profiles.__id} connectionId={peoplePagination.data.profiles.__id}
key={profile.id} key={profile.id}
fKey={profile} 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 { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
IconPencil,
IconTrashCan, IconTrashCan,
Option,
Select,
Spinner, Spinner,
Td, Td,
Tr, Tr,
useConfirm, useConfirm,
} from "@probo/ui"; } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useState } from "react"; import { use } from "react";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { type DataID, graphql } from "relay-runtime"; import { type DataID, graphql } from "relay-runtime";
import type { PeopleListItemFragment$key } from "#/__generated__/iam/PeopleListItemFragment.graphql"; import type { PeopleListItemFragment$key } from "#/__generated__/iam/PeopleListItemFragment.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CurrentUser } from "#/providers/CurrentUser";
import { EditMemberDialog } from "../../settings/_components/EditMemberDialog";
const fragment = graphql` const fragment = graphql`
fragment PeopleListItemFragment on Profile { fragment PeopleListItemFragment on Profile {
@@ -43,18 +43,29 @@ const fragment = graphql`
} }
`; `;
const removeMemberMutation = graphql` const updateRoleMutation = graphql`
mutation PeopleListItem_removeMutation( mutation PeopleListItem_updateRoleMutation($input: UpdateMembershipInput!) {
$input: RemoveMemberInput! updateMembership(input: $input) {
$connections: [ID!]! membership {
) { id
removeMember(input: $input) { role
deletedMembershipId @deleteEdge(connections: $connections) }
} }
} }
`; `;
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; connectionId: DataID;
fKey: PeopleListItemFragment$key; fKey: PeopleListItemFragment$key;
onRefetch: () => void; onRefetch: () => void;
@@ -64,27 +75,48 @@ export function MemberListItem(props: {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const confirm = useConfirm(); const confirm = useConfirm();
const [dialogOpen, setDialogOpen] = useState(false);
const { role } = use(CurrentUser);
const availableRoles = getAssignableRoles(role);
const profile = useFragment<PeopleListItemFragment$key>(fragment, fKey); const profile = useFragment<PeopleListItemFragment$key>(fragment, fKey);
const isInactive = profile.state === "INACTIVE"; const isInactive = profile.state === "INACTIVE";
const [removeMembership, isRemoving] = useMutationWithToasts( const [updateMembership, isUpdatingRole] = useMutationWithToasts(
removeMemberMutation, updateRoleMutation,
{ {
successMessage: __("Member removed successfully"), successMessage: __("Role updated successfully"),
errorMessage: __("Failed to remove member"), 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 = () => { const handleRemove = () => {
confirm( confirm(
() => { () => {
return removeMembership({ return removeUser({
variables: { variables: {
input: { input: {
membershipId: profile.membership.id, profileId: profile.id,
organizationId: organizationId, organizationId: organizationId,
}, },
connections: [connectionId], connections: [connectionId],
@@ -122,8 +154,28 @@ export function MemberListItem(props: {
</div> </div>
</Td> </Td>
<Td>{profile.kind}</Td> <Td>{profile.kind}</Td>
<Td> <Td noLink className="pr-4">
<Badge>{profile.membership.role}</Badge> <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>
<Td>{new Date(profile.createdAt).toLocaleDateString()}</Td> <Td>{new Date(profile.createdAt).toLocaleDateString()}</Td>
<Td>{profile.position}</Td> <Td>{profile.position}</Td>
@@ -133,15 +185,6 @@ export function MemberListItem(props: {
className="flex gap-2 justify-end" className="flex gap-2 justify-end"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
{profile.membership.canUpdate && (
<Button
variant="secondary"
onClick={() => setDialogOpen(true)}
disabled={dialogOpen}
icon={IconPencil}
aria-label={__("Edit role")}
/>
)}
{isRemoving {isRemoving
? ( ? (
<Spinner size={16} /> <Spinner size={16} />
@@ -161,13 +204,6 @@ export function MemberListItem(props: {
)} )}
</Td> </Td>
</Tr> </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 { 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 { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { type DataID, graphql } from "relay-runtime";
import { z } from "zod"; 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 { PersonFormFragment$key } from "#/__generated__/iam/PersonFormFragment.graphql";
import type { PersonFormMutation } from "#/__generated__/iam/PersonFormMutation.graphql";
import { ControlledField } from "#/components/form/ControlledField"; import { ControlledField } from "#/components/form/ControlledField";
import { EmailsField } from "#/components/form/EmailsField"; import { EmailsField } from "#/components/form/EmailsField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CurrentUser } from "#/providers/CurrentUser";
const fragment = graphql` const fragment = graphql`
fragment PersonFormFragment on Profile { fragment PersonFormFragment on Profile {
id id
fullName fullName
emailAddress
membership @required(action: THROW) {
role
}
kind kind
position position
additionalEmailAddresses 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` const updatePersonMutation = graphql`
mutation PersonFormMutation($input: UpdateProfileInput!) { mutation PersonForm_updateMutation($input: UpdateUserInput!) {
updateProfile(input: $input) { updateUser(input: $input) {
profile { profile {
id id
} }
@@ -37,7 +59,9 @@ const updatePersonMutation = graphql`
const schema = z.object({ const schema = z.object({
fullName: z.string().min(1), 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( additionalEmailAddresses: z.preprocess(
// Empty additional emails are skipped // Empty additional emails are skipped
v => (v as string[]).filter(v => !!v), v => (v as string[]).filter(v => !!v),
@@ -48,35 +72,59 @@ const schema = z.object({
contractEndDate: z.string().optional().nullable(), contractEndDate: z.string().optional().nullable(),
}); });
export function PersonForm(props: { fragmentRef: PersonFormFragment$key }) { export function PersonForm(props: {
const { fragmentRef } = 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 { __ } = useTranslate();
const { role } = use(CurrentUser);
const person = useFragment<PersonFormFragment$key>(fragment, fragmentRef); const availableRoles = getAssignableRoles(role);
const { control, formState, handleSubmit, register, reset } const { control, formState, handleSubmit, register, reset }
= useFormWithSchema(schema, { = useFormWithSchema(schema, { defaultValues });
defaultValues: { const watchedRole = useWatch({
kind: person.kind, control,
fullName: person.fullName, name: "role",
position: person.position, defaultValue: "EMPLOYEE",
additionalEmailAddresses: [...person.additionalEmailAddresses],
contractStartDate: person.contractStartDate?.split("T")[0] || "",
contractEndDate: person.contractEndDate?.split("T")[0] || "",
},
}); });
const [mutate, isMutating] = useMutationWithToasts<PersonFormMutation>( const [createPerson, isCreating] = useMutationWithToasts<PersonForm_createMutation>(
createPersonMutation,
{
successMessage: __("Person created successfully."),
errorMessage: __("Failed to create person"),
},
);
const [updatePerson, isUpdating] = useMutationWithToasts<PersonForm_updateMutation>(
updatePersonMutation, updatePersonMutation,
{ {
successMessage: __("Member updated successfully."), successMessage: __("Person updated successfully."),
errorMessage: __("Failed to update member"), errorMessage: __("Failed to update person"),
}, },
); );
const onSubmit = handleSubmit(async (data: z.infer<typeof schema>) => { const onSubmit = handleSubmit(async (data: z.infer<typeof schema>) => {
const input = { const input = {
id: person.id,
fullName: data.fullName, fullName: data.fullName,
emailAddress: data.emailAddress,
role: data.role,
additionalEmailAddresses: data.additionalEmailAddresses, additionalEmailAddresses: data.additionalEmailAddresses,
kind: data.kind, kind: data.kind,
position: data.position, position: data.position,
@@ -84,24 +132,90 @@ export function PersonForm(props: { fragmentRef: PersonFormFragment$key }) {
contractEndDate: formatDatetime(data.contractEndDate) ?? null, contractEndDate: formatDatetime(data.contractEndDate) ?? null,
}; };
await mutate({ if (id) {
variables: { input }, await updatePerson({
variables: { input: { ...input, id } },
onCompleted: () => { onCompleted: () => {
reset(data); reset(data);
}, },
}); });
} else {
await createPerson({
variables: {
input: { ...input, organizationId },
connections: [connectionId],
},
onCompleted: () => {
reset(data);
},
});
}
}); });
return ( return (
<form onSubmit={e => void onSubmit(e)} className="space-y-4"> <form onSubmit={e => void onSubmit(e)} className="space-y-4">
<Card padded className="space-y-4"> <Field label={__("Full name *")} {...register("fullName")} type="text" />
<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 <ControlledField
control={control} control={control}
name="kind" name="kind"
type="select" type="select"
label={__("Type")} label={__("Type")}
disabled={!person.canUpdate} disabled={disabled}
> >
{getRoles(__).map(role => ( {getRoles(__).map(role => (
<Option key={role.value} value={role.value}> <Option key={role.value} value={role.value}>
@@ -114,31 +228,55 @@ export function PersonForm(props: { fragmentRef: PersonFormFragment$key }) {
{...register("position")} {...register("position")}
type="text" type="text"
placeholder={__("e.g. CEO, CFO, etc.")} placeholder={__("e.g. CEO, CFO, etc.")}
disabled={!person.canUpdate} disabled={disabled}
/> />
<EmailsField control={control} register={register} /> <EmailsField control={control} register={register} />
<Field label={__("Contract start date")}> <Field label={__("Contract start date")}>
<Input <Input
{...register("contractStartDate")} {...register("contractStartDate")}
type="date" type="date"
disabled={!person.canUpdate} disabled={disabled}
/> />
</Field> </Field>
<Field label={__("Contract end date")}> <Field label={__("Contract end date")}>
<Input <Input
{...register("contractEndDate")} {...register("contractEndDate")}
type="date" type="date"
disabled={!person.canUpdate} disabled={disabled}
/> />
</Field> </Field>
</Card>
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && person.canUpdate && ( {(!id || formState.isDirty) && !disabled && (
<Button type="submit" disabled={isMutating}> <Button type="submit" disabled={isUpdating || isCreating || !formState.isValid}>
{__("Update")} {id ? __("Update") : __("Create")}
</Button> </Button>
)} )}
</div> </div>
</form> </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"; } from "./date";
export { getTrustCenterUrl } from "./trustCenter"; export { getTrustCenterUrl } from "./trustCenter";
export { formatError, type GraphQLError } from "./error"; export { formatError, type GraphQLError } from "./error";
export { Role, getAssignableRoles } from "./roles"; export { Role, roles, getAssignableRoles } from "./roles";
export { export {
getTrustCenterDocumentAccessInfo, getTrustCenterDocumentAccessInfo,
getTrustCenterDocumentAccessStatusBadgeVariant, getTrustCenterDocumentAccessStatusBadgeVariant,

View File

@@ -7,6 +7,13 @@ export const Role = {
} as const } as const
export type Role = (typeof Role)[keyof typeof Role]; export type Role = (typeof Role)[keyof typeof Role];
export const roles = [
"OWNER",
"ADMIN",
"VIEWER",
"AUDITOR",
"EMPLOYEE",
] as const
export function getAssignableRoles(currentRole: Role): Role[] { export function getAssignableRoles(currentRole: Role): Role[] {
if (currentRole === Role.OWNER) { if (currentRole === Role.OWNER) {

View File

@@ -18,6 +18,11 @@ WHERE
ALTER TABLE ALTER TABLE
iam_membership_profiles DROP COLUMN membership_id; iam_membership_profiles DROP COLUMN membership_id;
ALTER TABLE
iam_memberships
ALTER COLUMN
source DROP NOT NULL;
ALTER TABLE ALTER TABLE
iam_scim_events iam_scim_events
ADD ADD

View File

@@ -164,7 +164,9 @@ var IAMOwnerPolicy = policy.NewPolicy(
policy.Allow( policy.Allow(
ActionMembershipProfileGet, ActionMembershipProfileGet,
ActionMembershipProfileList, ActionMembershipProfileList,
ActionMembershipProfileCreate,
ActionMembershipProfileUpdate, ActionMembershipProfileUpdate,
ActionMembershipProfileDelete,
). ).
WithSID("full-membership-profile-access"). WithSID("full-membership-profile-access").
When(policy.Equals("principal.organization_id", "resource.organization_id")), When(policy.Equals("principal.organization_id", "resource.organization_id")),
@@ -243,7 +245,9 @@ var IAMAdminPolicy = policy.NewPolicy(
policy.Allow( policy.Allow(
ActionMembershipProfileGet, ActionMembershipProfileGet,
ActionMembershipProfileList, ActionMembershipProfileList,
ActionMembershipProfileCreate,
ActionMembershipProfileUpdate, ActionMembershipProfileUpdate,
ActionMembershipProfileDelete,
). ).
WithSID("membership-profile-admin-access"). WithSID("membership-profile-admin-access").
When(policy.Equals("principal.organization_id", "resource.organization_id")), 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() now = time.Now()
) )
err := s.pg.WithConn( err := s.pg.WithTx(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
identity := &coredata.Identity{} identity := &coredata.Identity{}
@@ -966,6 +966,9 @@ func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserReq
profile = &coredata.MembershipProfile{ profile = &coredata.MembershipProfile{
ID: gid.New(req.OrganizationID.TenantID(), coredata.MembershipProfileEntityType), ID: gid.New(req.OrganizationID.TenantID(), coredata.MembershipProfileEntityType),
IdentityID: identity.ID,
OrganizationID: req.OrganizationID,
Source: coredata.ProfileSourceManual,
FullName: req.FullName, FullName: req.FullName,
Kind: req.Kind, Kind: req.Kind,
AdditionalEmailAddresses: req.AdditionalEmailAddresses, AdditionalEmailAddresses: req.AdditionalEmailAddresses,

View File

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

View File

@@ -294,6 +294,7 @@ type ComplexityRoot struct {
ContractEndDate func(childComplexity int) int ContractEndDate func(childComplexity int) int
ContractStartDate func(childComplexity int) int ContractStartDate func(childComplexity int) int
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
EmailAddress func(childComplexity int) int
FullName func(childComplexity int) int FullName func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Identity 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 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": case "Profile.fullName":
if e.complexity.Profile.FullName == nil { if e.complexity.Profile.FullName == nil {
break break
@@ -2708,6 +2715,7 @@ type Identity implements Node {
type Profile implements Node { type Profile implements Node {
id: ID! id: ID!
fullName: String! fullName: String!
emailAddress: EmailAddr!
source: String! source: String!
state: ProfileState! state: ProfileState!
additionalEmailAddresses: [EmailAddr!]! additionalEmailAddresses: [EmailAddr!]!
@@ -9003,6 +9011,8 @@ func (ec *executionContext) fieldContext_Organization_viewer(_ context.Context,
return ec.fieldContext_Profile_id(ctx, field) return ec.fieldContext_Profile_id(ctx, field)
case "fullName": case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field) return ec.fieldContext_Profile_fullName(ctx, field)
case "emailAddress":
return ec.fieldContext_Profile_emailAddress(ctx, field)
case "source": case "source":
return ec.fieldContext_Profile_source(ctx, field) return ec.fieldContext_Profile_source(ctx, field)
case "state": case "state":
@@ -9796,6 +9806,35 @@ func (ec *executionContext) fieldContext_Profile_fullName(_ context.Context, fie
return fc, nil 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) { func (ec *executionContext) _Profile_source(ctx context.Context, field graphql.CollectedField, obj *types.Profile) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -10433,6 +10472,8 @@ func (ec *executionContext) fieldContext_ProfileEdge_node(_ context.Context, fie
return ec.fieldContext_Profile_id(ctx, field) return ec.fieldContext_Profile_id(ctx, field)
case "fullName": case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field) return ec.fieldContext_Profile_fullName(ctx, field)
case "emailAddress":
return ec.fieldContext_Profile_emailAddress(ctx, field)
case "source": case "source":
return ec.fieldContext_Profile_source(ctx, field) return ec.fieldContext_Profile_source(ctx, field)
case "state": case "state":
@@ -13907,6 +13948,8 @@ func (ec *executionContext) fieldContext_UpdateUserPayload_profile(_ context.Con
return ec.fieldContext_Profile_id(ctx, field) return ec.fieldContext_Profile_id(ctx, field)
case "fullName": case "fullName":
return ec.fieldContext_Profile_fullName(ctx, field) return ec.fieldContext_Profile_fullName(ctx, field)
case "emailAddress":
return ec.fieldContext_Profile_emailAddress(ctx, field)
case "source": case "source":
return ec.fieldContext_Profile_source(ctx, field) return ec.fieldContext_Profile_source(ctx, field)
case "state": case "state":
@@ -19237,6 +19280,11 @@ func (ec *executionContext) _Profile(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) 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": case "source":
out.Values[i] = ec._Profile_source(ctx, field, obj) out.Values[i] = ec._Profile_source(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {

View File

@@ -310,6 +310,7 @@ type PersonalAPIKeyEdge struct {
type Profile struct { type Profile struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
EmailAddress mail.Addr `json:"emailAddress"`
Source string `json:"source"` Source string `json:"source"`
State coredata.ProfileState `json:"state"` State coredata.ProfileState `json:"state"`
AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses"` 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()) 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) return nil, gqlutils.Internal(ctx)
} }