Plug back organization members

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-26 22:57:56 +01:00
committed by Bryan Frimin
parent ac85d6b84d
commit e0f6bbd204
22 changed files with 2555 additions and 23 deletions

View File

@@ -0,0 +1,155 @@
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { MemberList } from "./_components/MemberList";
import type { MembersPageQuery } from "./__generated__/MembersPageQuery.graphql";
import { useTranslate } from "@probo/i18n";
import { Card, TabBadge, TabItem, Tabs } from "@probo/ui";
import { useState } from "react";
export const membersPageQuery = graphql`
query MembersPageQuery($organizationId: ID!) {
viewer @required(action: THROW) {
...MemberListItem_permissionsFragment
@arguments(organizationId: $organizationId)
}
organization: node(id: $organizationId) @required(action: THROW) {
__typename
... on Organization {
...MemberListFragment
@arguments(first: 20, order: { direction: ASC, field: CREATED_AT })
members(first: 20, orderBy: { direction: ASC, field: CREATED_AT })
@required(action: THROW) {
totalCount
}
}
}
}
`;
export function MembersPage(props: {
queryRef: PreloadedQuery<MembersPageQuery>;
}) {
const { queryRef } = props;
const { __ } = useTranslate();
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">(
"memberships",
);
const { organization, viewer } = usePreloadedQuery<MembersPageQuery>(
membersPageQuery,
queryRef,
);
if (organization.__typename !== "Organization") {
throw new Error("node is of invalid type");
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
{/* {isAuthorized("Organization", "inviteUser") && (
<InviteUserDialog
connectionId={invitationsPagination.data.invitations?.__id}
onRefetch={refetchInvitations}
>
<Button variant="secondary">{__("Invite member")}</Button>
</InviteUserDialog>
)} */}
</div>
<Tabs>
<TabItem
active={activeTab === "memberships"}
onClick={() => setActiveTab("memberships")}
>
{__("Members")}
{(organization.members.totalCount ?? 0) > 0 && (
<TabBadge>{organization.members.totalCount}</TabBadge>
)}
</TabItem>
{/* <TabItem
active={activeTab === "invitations"}
onClick={() => setActiveTab("invitations")}
>
{__("Invitations")}
{(invitationsPagination.data.invitations?.totalCount || 0) > 0 && (
<TabBadge>
{invitationsPagination.data.invitations?.totalCount}
</TabBadge>
)}
</TabItem> */}
</Tabs>
<Card>
<div className="px-6 pb-6 pt-6">
{activeTab === "memberships" && (
<MemberList fKey={organization} viewerFKey={viewer} />
)}
{/* {activeTab === "invitations" && (
<SortableTable
{...invitationsPagination}
refetch={({
order,
}: {
order: { direction: string; field: string };
}) => {
invitationsPagination.refetch({
order: {
direction: order.direction as "ASC" | "DESC",
field: order.field as
| "CREATED_AT"
| "EXPIRES_AT"
| "FULL_NAME"
| "EMAIL"
| "ROLE"
| "STATUS"
| "ACCEPTED_AT",
},
});
}}
pageSize={20}
>
<Thead>
<Tr>
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
<SortableTh field="ROLE">{__("Role")}</SortableTh>
<SortableTh field="CREATED_AT">{__("Invited")}</SortableTh>
<Th>{__("Status")}</Th>
<SortableTh field="ACCEPTED_AT">
{__("Accepted at")}
</SortableTh>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{invitations.length === 0 ? (
<Tr>
<Td colSpan={7} className="text-center text-txt-secondary">
{__("No invitations")}
</Td>
</Tr>
) : (
invitations.map((invitation) => (
<InvitationRow
key={invitation.id}
invitation={invitation}
connectionId={
invitationsPagination.data.invitations?.__id
}
organizationId={(organizationKey as { id: string }).id}
onRefetch={refetchInvitations}
/>
))
)}
</Tbody>
</SortableTable>
)} */}
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { useQueryLoader } from "react-relay";
import { MembersPage, membersPageQuery } from "./MembersPage";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { IAMRelayProvider } from "/providers/IAMRelayProvider";
import { useEffect } from "react";
import type { MembersPageQuery } from "./__generated__/MembersPageQuery.graphql";
function MembersPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] =
useQueryLoader<MembersPageQuery>(membersPageQuery);
useEffect(() => {
loadQuery({
organizationId,
});
}, [loadQuery, organizationId]);
if (!queryRef) {
return null;
}
return <MembersPage queryRef={queryRef} />;
}
export default function () {
return (
<IAMRelayProvider>
<MembersPageLoader />
</IAMRelayProvider>
);
}

View File

@@ -0,0 +1,431 @@
/**
* @generated SignedSource<<e16e82aa25ced17faf9e4c40f7e6130e>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MembersPageQuery$variables = {
organizationId: string;
};
export type MembersPageQuery$data = {
readonly organization: {
readonly __typename: "Organization";
readonly members: {
readonly totalCount: number | null | undefined;
};
readonly " $fragmentSpreads": FragmentRefs<"MemberListFragment">;
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
readonly viewer: {
readonly " $fragmentSpreads": FragmentRefs<"MemberListItem_permissionsFragment">;
};
};
export type MembersPageQuery = {
response: MembersPageQuery$data;
variables: MembersPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = {
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
},
v2 = [
(v1/*: any*/)
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = {
"kind": "Literal",
"name": "first",
"value": 20
},
v5 = {
"direction": "ASC",
"field": "CREATED_AT"
},
v6 = [
(v4/*: any*/),
{
"kind": "Literal",
"name": "orderBy",
"value": (v5/*: any*/)
}
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "MembersPageQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"args": [
{
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
}
],
"kind": "FragmentSpread",
"name": "MemberListItem_permissionsFragment"
}
],
"storageKey": null
},
"action": "THROW"
},
{
"kind": "RequiredField",
"field": {
"alias": "organization",
"args": (v2/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"args": [
(v4/*: any*/),
{
"kind": "Literal",
"name": "order",
"value": (v5/*: any*/)
}
],
"kind": "FragmentSpread",
"name": "MemberListFragment"
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": (v6/*: any*/),
"concreteType": "MembershipConnection",
"kind": "LinkedField",
"name": "members",
"plural": false,
"selections": [
(v7/*: any*/)
],
"storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "MembersPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"alias": "canUpdateMembership",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership:update"
},
(v1/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
{
"alias": "canDeleteMembership",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership:delete"
},
(v1/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v2/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v8/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
(v9/*: any*/),
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": "MembershipConnection",
"kind": "LinkedField",
"name": "members",
"plural": false,
"selections": [
(v7/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "MembershipEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v8/*: any*/),
(v9/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "MembershipProfile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
(v8/*: any*/)
],
"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
},
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": "members(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
},
{
"alias": null,
"args": (v6/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "MembersListFragment_members",
"kind": "LinkedHandle",
"name": "members"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "b125343184bf4d82a7d0e264faf7ac34",
"id": null,
"metadata": {},
"name": "MembersPageQuery",
"operationKind": "query",
"text": "query MembersPageQuery(\n $organizationId: ID!\n) {\n viewer {\n ...MemberListItem_permissionsFragment_4xMPKw\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MemberListFragment_1jRT0c\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n }\n }\n id\n }\n}\n\nfragment MemberListFragment_1jRT0c on Organization {\n ...MemberListItem_currentRoleFragment\n members(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n}\n\nfragment MemberListItem_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n\nfragment MemberListItem_permissionsFragment_4xMPKw on Identity {\n canUpdateMembership: permission(action: \"iam:membership:update\", id: $organizationId)\n canDeleteMembership: permission(action: \"iam:membership:delete\", id: $organizationId)\n}\n"
}
};
})();
(node as any).hash = "8c40640ce96580ebae677d117741094e";
export default node;

View File

@@ -0,0 +1,131 @@
import { getAssignableRoles, Role, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
DialogContent,
Field,
Select,
Option,
DialogFooter,
Button,
Spinner,
Dialog,
} from "@probo/ui";
import { useState } from "react";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { MemberListItemFragment$data } from "./__generated__/MemberListItemFragment.graphql";
const updateMembershipMutation = graphql`
mutation EditMemberDialog_updateMutation($input: UpdateMembershipInput!) {
updateMembership(input: $input) {
membership {
id
role
}
}
}
`;
export function EditMemberDialog(props: {
currentRole: Role;
membership: MemberListItemFragment$data;
onClose: () => void;
}) {
const { currentRole, membership, onClose } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const availableRoles = getAssignableRoles(currentRole);
const [selectedRole, setSelectedRole] = useState<string>(membership.role);
const [updateMembership, isUpdating] = useMutationWithToasts(
updateMembershipMutation,
{
successMessage: __("Role updated successfully"),
errorMessage: __("Failed to update role"),
},
);
const handleUpdateRole = (e: React.FormEvent) => {
e.preventDefault();
updateMembership({
variables: {
input: {
membershipId: membership.id,
organizationId: organizationId,
role: selectedRole,
},
},
onCompleted: onClose,
});
};
return (
<Dialog defaultOpen={true} onClose={onClose} title={__("Edit Member Role")}>
<form onSubmit={handleUpdateRole}>
<DialogContent padded className="space-y-6">
<div>
<p className="text-txt-secondary text-sm mb-4">
{sprintf(
__("Update the role for %s"),
membership.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 === membership.role}
>
{isUpdating && <Spinner />}
{__("Update Role")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,108 @@
import { graphql, usePaginationFragment } from "react-relay";
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { SortableTable, SortableTh } from "/components/SortableTable";
import type { MemberListFragment_RefetchQuery } from "./__generated__/MemberListFragment_RefetchQuery.graphql";
import { MemberListItem } from "./MemberListItem";
import type { MemberListFragment$key } from "./__generated__/MemberListFragment.graphql";
import type { MemberListItem_permissionsFragment$key } from "./__generated__/MemberListItem_permissionsFragment.graphql";
const fragment = graphql`
fragment MemberListFragment on Organization
@refetchable(queryName: "MemberListFragment_RefetchQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 20 }
order: {
type: "MembershipOrder"
defaultValue: { direction: ASC, field: CREATED_AT }
}
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
...MemberListItem_currentRoleFragment
members(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "MembersListFragment_members") @required(action: THROW) {
__id
totalCount
edges @required(action: THROW) {
node {
id
...MemberListItemFragment
}
}
}
}
`;
export function MemberList(props: {
fKey: MemberListFragment$key;
viewerFKey: MemberListItem_permissionsFragment$key;
}) {
const { fKey, viewerFKey } = props;
const { __ } = useTranslate();
const membersPagination = usePaginationFragment<
MemberListFragment_RefetchQuery,
MemberListFragment$key
>(fragment, fKey);
const refetchMemberships = () => {
membersPagination.refetch({}, { fetchPolicy: "network-only" });
};
return (
<SortableTable
{...membersPagination}
refetch={({ order }: { order: { direction: string; field: string } }) => {
membersPagination.refetch({
order: {
direction: order.direction as "ASC" | "DESC",
field: order.field as
| "CREATED_AT"
// FIXME: add those back
// | "FULL_NAME"
// | "EMAIL_ADDRESS"
| "ROLE",
},
});
}}
pageSize={20}
>
<Thead>
<Tr>
<SortableTh field="FULL_NAME">{__("Name")}</SortableTh>
<SortableTh field="EMAIL_ADDRESS">{__("Email")}</SortableTh>
<SortableTh field="ROLE">{__("Role")}</SortableTh>
<SortableTh field="CREATED_AT">{__("Joined")}</SortableTh>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{membersPagination.data.members.edges.length === 0 ? (
<Tr>
<Td colSpan={5} className="text-center text-txt-secondary">
{__("No members")}
</Td>
</Tr>
) : (
membersPagination.data.members.edges.map(({ node: membership }) => (
<MemberListItem
key={membership.id}
fKey={membership}
permissionsFKey={membersPagination.data}
onRefetch={refetchMemberships}
viewerFKey={viewerFKey}
/>
))
)}
</Tbody>
</SortableTable>
);
}

View File

@@ -0,0 +1,194 @@
import { useTranslate } from "@probo/i18n";
import {
Badge,
Button,
IconPencil,
IconTrashCan,
Spinner,
Td,
Tr,
useConfirm,
} from "@probo/ui";
import clsx from "clsx";
import { useState } from "react";
import { useFragment } from "react-relay";
import { ConnectionHandler, graphql } from "relay-runtime";
import type { MemberListItemFragment$key } from "./__generated__/MemberListItemFragment.graphql";
import type { MemberListItem_permissionsFragment$key } from "./__generated__/MemberListItem_permissionsFragment.graphql";
import type { MemberListItem_currentRoleFragment$key } from "./__generated__/MemberListItem_currentRoleFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { sprintf } from "@probo/helpers";
import { EditMemberDialog } from "./EditMemberDialog";
const fragment = graphql`
fragment MemberListItemFragment on Membership {
id
role
profile @required(action: THROW) {
fullName
}
identity @required(action: THROW) {
email
}
createdAt
}
`;
const currentRoleFragment = graphql`
fragment MemberListItem_currentRoleFragment on Organization {
viewerMembership @required(action: THROW) {
role
}
}
`;
const permissionsFragment = graphql`
fragment MemberListItem_permissionsFragment on Identity
@argumentDefinitions(organizationId: { type: "ID!" }) {
canUpdateMembership: permission(
action: "iam:membership:update"
id: $organizationId
)
canDeleteMembership: permission(
action: "iam:membership:delete"
id: $organizationId
)
}
`;
const removeMemberMutation = graphql`
mutation MemberListItem_removeMutation(
$input: RemoveMemberInput!
$connections: [ID!]!
) {
removeMember(input: $input) {
deletedMembershipId @deleteEdge(connections: $connections)
}
}
`;
export function MemberListItem(props: {
fKey: MemberListItemFragment$key;
permissionsFKey: MemberListItem_currentRoleFragment$key;
viewerFKey: MemberListItem_permissionsFragment$key;
onRefetch: () => void;
}) {
const { fKey, onRefetch, permissionsFKey, viewerFKey } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const confirm = useConfirm();
const [dialogOpen, setDialogOpen] = useState(false);
const membership = useFragment<MemberListItemFragment$key>(fragment, fKey);
const { viewerMembership } =
useFragment<MemberListItem_currentRoleFragment$key>(
currentRoleFragment,
permissionsFKey,
);
const permissions = useFragment<MemberListItem_permissionsFragment$key>(
permissionsFragment,
viewerFKey,
);
// Only OWNER can edit OWNER members
const canEditThisRole =
membership.role === "OWNER" ? viewerMembership.role === "OWNER" : true;
const [removeMembership, isRemoving] = useMutationWithToasts(
removeMemberMutation,
{
successMessage: __("Member removed successfully"),
errorMessage: __("Failed to remove member"),
},
);
const handleRemove = async () => {
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
"MembersListFragment_members",
);
confirm(
() => {
return removeMembership({
variables: {
input: {
membershipId: membership.id,
organizationId: organizationId,
},
connections: [connectionId],
},
onCompleted: onRefetch,
});
},
{
message: sprintf(
__("Are you sure you want to remove %s?"),
membership.profile.fullName,
),
},
);
};
return (
<>
<Tr className={clsx(isRemoving && "opacity-60 pointer-events-none")}>
<Td>
<div className="font-semibold">{membership.profile.fullName}</div>
</Td>
<Td>
<div className="flex items-center gap-2">
{membership.identity.email}
{/* FIXME: put back */}
{/* {membership.authMethod === "SAML" && (
<Badge variant="info">SAML</Badge>
)} */}
</div>
</Td>
<Td>
<Badge>{membership.role}</Badge>
</Td>
<Td>{new Date(membership.createdAt).toLocaleDateString()}</Td>
<Td noLink width={160} className="text-end">
<div
className="flex gap-2 justify-end"
onClick={(e) => e.stopPropagation()}
>
{permissions.canUpdateMembership && canEditThisRole && (
<Button
variant="secondary"
onClick={() => setDialogOpen(true)}
disabled={dialogOpen}
icon={IconPencil}
aria-label={__("Edit role")}
/>
)}
{isRemoving ? (
<Spinner size={16} />
) : (
permissions.canDeleteMembership &&
canEditThisRole && (
<Button
variant="danger"
onClick={handleRemove}
disabled={isRemoving}
icon={IconTrashCan}
aria-label={__("Remove member")}
/>
)
)}
</div>
</Td>
</Tr>
{dialogOpen && (
<EditMemberDialog
currentRole={viewerMembership.role}
onClose={() => setDialogOpen(false)}
membership={membership}
/>
)}
</>
);
}

View File

@@ -0,0 +1,116 @@
/**
* @generated SignedSource<<d29dfbdd0293d60af6d3f72e09dd5e9d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type UpdateMembershipInput = {
membershipId: string;
organizationId: string;
role: MembershipRole;
};
export type EditMemberDialog_updateMutation$variables = {
input: UpdateMembershipInput;
};
export type EditMemberDialog_updateMutation$data = {
readonly updateMembership: {
readonly membership: {
readonly id: string;
readonly role: MembershipRole;
};
};
};
export type EditMemberDialog_updateMutation = {
response: EditMemberDialog_updateMutation$data;
variables: EditMemberDialog_updateMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateMembershipPayload",
"kind": "LinkedField",
"name": "updateMembership",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "membership",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditMemberDialog_updateMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditMemberDialog_updateMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "696ec9f74220b92171bf60630e5c8d7f",
"id": null,
"metadata": {},
"name": "EditMemberDialog_updateMutation",
"operationKind": "mutation",
"text": "mutation EditMemberDialog_updateMutation(\n $input: UpdateMembershipInput!\n) {\n updateMembership(input: $input) {\n membership {\n id\n role\n }\n }\n}\n"
}
};
})();
(node as any).hash = "fc971ff69092912586c14c7f6e2111ed";
export default node;

View File

@@ -0,0 +1,250 @@
/**
* @generated SignedSource<<f1134e8dcf977b50bc30bb957ec4a007>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MemberListFragment$data = {
readonly id: string;
readonly members: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"MemberListItemFragment">;
};
}>;
readonly totalCount: number | null | undefined;
};
readonly " $fragmentSpreads": FragmentRefs<"MemberListItem_currentRoleFragment">;
readonly " $fragmentType": "MemberListFragment";
};
export type MemberListFragment$key = {
readonly " $data"?: MemberListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MemberListFragment">;
};
import MemberListFragment_RefetchQuery_graphql from './MemberListFragment_RefetchQuery.graphql';
const node: ReaderFragment = (function(){
var v0 = [
"members"
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
{
"defaultValue": {
"direction": "ASC",
"field": "CREATED_AT"
},
"kind": "LocalArgument",
"name": "order"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "bidirectional",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "first",
"cursor": "after"
},
"backward": {
"count": "last",
"cursor": "before"
},
"path": (v0/*: any*/)
},
"fragmentPathInResult": [
"node"
],
"operation": MemberListFragment_RefetchQuery_graphql,
"identifierInfo": {
"identifierField": "id",
"identifierQueryVariableName": "id"
}
}
},
"name": "MemberListFragment",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "MemberListItem_currentRoleFragment"
},
{
"kind": "RequiredField",
"field": {
"alias": "members",
"args": [
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
],
"concreteType": "MembershipConnection",
"kind": "LinkedField",
"name": "__MembersListFragment_members_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "MembershipEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "MemberListItemFragment"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
},
"action": "THROW"
},
(v1/*: any*/)
],
"type": "Organization",
"abstractKey": null
};
})();
(node as any).hash = "dcdc5a8acd5e8f7317fbd1bcd3570f61";
export default node;

View File

@@ -0,0 +1,392 @@
/**
* @generated SignedSource<<4966da1a2a597f71a90c8766ad760615>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MembershipOrderField = "CREATED_AT" | "ROLE";
export type OrderDirection = "ASC" | "DESC";
export type MembershipOrder = {
direction: OrderDirection;
field: MembershipOrderField;
};
export type MemberListFragment_RefetchQuery$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
first?: number | null | undefined;
id: string;
last?: number | null | undefined;
order?: MembershipOrder | null | undefined;
};
export type MemberListFragment_RefetchQuery$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"MemberListFragment">;
} | null | undefined;
};
export type MemberListFragment_RefetchQuery = {
response: MemberListFragment_RefetchQuery$data;
variables: MemberListFragment_RefetchQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
v2 = {
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
v3 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
},
v4 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
v5 = {
"defaultValue": {
"direction": "ASC",
"field": "CREATED_AT"
},
"kind": "LocalArgument",
"name": "order"
},
v6 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v7 = {
"kind": "Variable",
"name": "after",
"variableName": "after"
},
v8 = {
"kind": "Variable",
"name": "before",
"variableName": "before"
},
v9 = {
"kind": "Variable",
"name": "first",
"variableName": "first"
},
v10 = {
"kind": "Variable",
"name": "last",
"variableName": "last"
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
},
v14 = [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
];
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MemberListFragment_RefetchQuery",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"kind": "Variable",
"name": "order",
"variableName": "order"
}
],
"kind": "FragmentSpread",
"name": "MemberListFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v3/*: any*/)
],
"kind": "Operation",
"name": "MemberListFragment_RefetchQuery",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v11/*: any*/),
(v12/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
(v13/*: any*/),
(v12/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v14/*: any*/),
"concreteType": "MembershipConnection",
"kind": "LinkedField",
"name": "members",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "MembershipEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v12/*: any*/),
(v13/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "MembershipProfile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
(v12/*: any*/)
],
"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
},
(v12/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
(v11/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
},
{
"alias": null,
"args": (v14/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "MembersListFragment_members",
"kind": "LinkedHandle",
"name": "members"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "df1f40dcee56d6e5181849cd82acb824",
"id": null,
"metadata": {},
"name": "MemberListFragment_RefetchQuery",
"operationKind": "query",
"text": "query MemberListFragment_RefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MemberListFragment_16fISc\n id\n }\n}\n\nfragment MemberListFragment_16fISc on Organization {\n ...MemberListItem_currentRoleFragment\n members(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n ...MemberListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment MemberListItemFragment on Membership {\n id\n role\n profile {\n fullName\n id\n }\n identity {\n email\n id\n }\n createdAt\n}\n\nfragment MemberListItem_currentRoleFragment on Organization {\n viewerMembership {\n role\n id\n }\n}\n"
}
};
})();
(node as any).hash = "dcdc5a8acd5e8f7317fbd1bcd3570f61";
export default node;

View File

@@ -0,0 +1,109 @@
/**
* @generated SignedSource<<50f7d8bffdec238d8a262e892db7be39>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
import { FragmentRefs } from "relay-runtime";
export type MemberListItemFragment$data = {
readonly createdAt: any;
readonly id: string;
readonly identity: {
readonly email: any;
};
readonly profile: {
readonly fullName: string;
};
readonly role: MembershipRole;
readonly " $fragmentType": "MemberListItemFragment";
};
export type MemberListItemFragment$key = {
readonly " $data"?: MemberListItemFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MemberListItemFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "MemberListItemFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "MembershipProfile",
"kind": "LinkedField",
"name": "profile",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "identity",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"type": "Membership",
"abstractKey": null
};
(node as any).hash = "31ca1c20d23f9709b0c1575ae56aeb64";
export default node;

View File

@@ -0,0 +1,60 @@
/**
* @generated SignedSource<<89f39cf7aa4cd7702b62f549ed8defdd>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
import { FragmentRefs } from "relay-runtime";
export type MemberListItem_currentRoleFragment$data = {
readonly viewerMembership: {
readonly role: MembershipRole;
};
readonly " $fragmentType": "MemberListItem_currentRoleFragment";
};
export type MemberListItem_currentRoleFragment$key = {
readonly " $data"?: MemberListItem_currentRoleFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MemberListItem_currentRoleFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "MemberListItem_currentRoleFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "9e7e3eefcb6f2f4398b0703a9342548b";
export default node;

View File

@@ -0,0 +1,77 @@
/**
* @generated SignedSource<<66b5754d2d4ecfc54f6c698df247e241>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MemberListItem_permissionsFragment$data = {
readonly canDeleteMembership: boolean;
readonly canUpdateMembership: boolean;
readonly " $fragmentType": "MemberListItem_permissionsFragment";
};
export type MemberListItem_permissionsFragment$key = {
readonly " $data"?: MemberListItem_permissionsFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MemberListItem_permissionsFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
};
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
"kind": "Fragment",
"metadata": null,
"name": "MemberListItem_permissionsFragment",
"selections": [
{
"alias": "canUpdateMembership",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership:update"
},
(v0/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
{
"alias": "canDeleteMembership",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "iam:membership:delete"
},
(v0/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
}
],
"type": "Identity",
"abstractKey": null
};
})();
(node as any).hash = "d467db6a9f7f80521cf35c8c1a0c3416";
export default node;

View File

@@ -0,0 +1,133 @@
/**
* @generated SignedSource<<1eebc63e903c6dd5ab9f0178d2aaaa90>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RemoveMemberInput = {
membershipId: string;
organizationId: string;
};
export type MemberListItem_removeMutation$variables = {
connections: ReadonlyArray<string>;
input: RemoveMemberInput;
};
export type MemberListItem_removeMutation$data = {
readonly removeMember: {
readonly deletedMembershipId: string;
} | null | undefined;
};
export type MemberListItem_removeMutation = {
response: MemberListItem_removeMutation$data;
variables: MemberListItem_removeMutation$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": "deletedMembershipId",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MemberListItem_removeMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "RemoveMemberPayload",
"kind": "LinkedField",
"name": "removeMember",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MemberListItem_removeMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "RemoveMemberPayload",
"kind": "LinkedField",
"name": "removeMember",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "deleteEdge",
"key": "",
"kind": "ScalarHandle",
"name": "deletedMembershipId",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6bf37e12e069385eabd2bba63e0e980f",
"id": null,
"metadata": {},
"name": "MemberListItem_removeMutation",
"operationKind": "mutation",
"text": "mutation MemberListItem_removeMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMembershipId\n }\n}\n"
}
};
})();
(node as any).hash = "9732b5ce9ca9de5c31e71a690e1763ed";
export default node;

View File

@@ -208,12 +208,13 @@ const routes = [
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader.tsx"),
),
},
// {
// path: "members",
// Component: lazy(
// () => import("./pages/organizations/settings/MembersSettingsTab")
// ),
// },
{
path: "members",
Component: lazy(
() =>
import("./pages/iam/organizations/settings/MembersPageLoader.tsx"),
),
},
{
path: "domain",
Component: lazy(

View File

@@ -503,7 +503,7 @@ WITH membership_with_profile AS (
iam_membership_profiles mp ON mp.membership_id = m.id
WHERE
m.organization_id = @organization_id
AND %s
AND m.%s
)
SELECT
id,

View File

@@ -51,6 +51,7 @@ const (
ActionIAMMembershipGet = "iam:membership:get"
ActionIAMMembershipLit = "iam:membership:list"
ActionIAMMembershipUpdate = "iam:membership:update"
ActionIAMMembershipDelete = "iam:membership:delete"
// Personal API Key actions
ActionIAMPersonalAPIKeyCreate = "iam:personal-api-key:create"

View File

@@ -155,6 +155,73 @@ func NewOrganizationService(svc *Service) *OrganizationService {
return &OrganizationService{Service: svc}
}
func (s *OrganizationService) CountMemberships(
ctx context.Context,
organizationID gid.GID,
) (int, error) {
var count int
scope := coredata.NewScopeFromObjectID(organizationID)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
memberships := coredata.Memberships{}
count, err = memberships.CountByOrganizationID(ctx, conn, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count memberships: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *OrganizationService) UpdateMempership(
ctx context.Context,
organizationID gid.GID,
membershipID gid.GID,
role coredata.MembershipRole,
) (*coredata.Membership, error) {
scope := coredata.NewScopeFromObjectID(organizationID)
membership := coredata.Membership{}
if err := s.pg.WithTx(ctx,
func(tx pg.Conn) error {
if err := membership.LoadByID(ctx, tx, scope, membershipID); err != nil {
if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(membershipID)
}
return fmt.Errorf("cannot load membership: %w", err)
}
if membership.OrganizationID != organizationID {
return NewMembershipNotFoundError(membership.ID)
}
membership.Role = role
membership.UpdatedAt = time.Now()
if err := membership.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update membership: %w", err)
}
return nil
},
); err != nil {
return nil, err
}
return &membership, nil
}
func (s *OrganizationService) RemoveMember(
ctx context.Context,
organizationID gid.GID,

View File

@@ -110,6 +110,7 @@ type Mutation {
@session(required: PRESENT)
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload
@session(required: PRESENT)
updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload!
removeMember(input: RemoveMemberInput!): RemoveMemberPayload
@session(required: PRESENT)
@@ -232,7 +233,7 @@ type Membership implements Node {
id: ID!
createdAt: Datetime!
identity: Identity @goField(forceResolver: true)
profile: MembershipProfile @goField(forceResolver: true) @isViewer
profile: MembershipProfile @goField(forceResolver: true)
organization: Organization @goField(forceResolver: true)
role: MembershipRole!
permissions: [Permission!] @goField(forceResolver: true)
@@ -601,6 +602,12 @@ input InviteMemberInput {
fullName: String!
}
input UpdateMembershipInput {
organizationId: ID!
membershipId: ID!
role: MembershipRole!
}
input RemoveMemberInput {
organizationId: ID!
membershipId: ID!
@@ -754,6 +761,10 @@ type InviteMemberPayload {
invitationEdge: InvitationEdge!
}
type UpdateMembershipPayload {
membership: Membership!
}
type RemoveMemberPayload {
deletedMembershipId: ID!
}

View File

@@ -220,6 +220,7 @@ type ComplexityRoot struct {
SignOut func(childComplexity int) int
SignUp func(childComplexity int, input types.SignUpInput) int
SignUpFromInvitation func(childComplexity int, input types.SignUpFromInvitationInput) int
UpdateMembership func(childComplexity int, input types.UpdateMembershipInput) int
UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int
UpdatePersonalAPIKey func(childComplexity int, input types.UpdatePersonalAPIKeyInput) int
UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int
@@ -399,6 +400,10 @@ type ComplexityRoot struct {
Identity func(childComplexity int) int
}
UpdateMembershipPayload struct {
Membership func(childComplexity int) int
}
UpdateOrganizationPayload struct {
Organization func(childComplexity int) int
}
@@ -462,6 +467,7 @@ type MutationResolver interface {
DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error)
InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error)
DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error)
UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error)
RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error)
AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error)
CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error)
@@ -1163,6 +1169,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Mutation.SignUpFromInvitation(childComplexity, args["input"].(types.SignUpFromInvitationInput)), true
case "Mutation.updateMembership":
if e.complexity.Mutation.UpdateMembership == nil {
break
}
args, err := ec.field_Mutation_updateMembership_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateMembership(childComplexity, args["input"].(types.UpdateMembershipInput)), true
case "Mutation.updateOrganization":
if e.complexity.Mutation.UpdateOrganization == nil {
break
@@ -1812,6 +1829,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.SignUpPayload.Identity(childComplexity), true
case "UpdateMembershipPayload.membership":
if e.complexity.UpdateMembershipPayload.Membership == nil {
break
}
return e.complexity.UpdateMembershipPayload.Membership(childComplexity), true
case "UpdateOrganizationPayload.organization":
if e.complexity.UpdateOrganizationPayload.Organization == nil {
break
@@ -1872,6 +1896,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputSignInInput,
ec.unmarshalInputSignUpFromInvitationInput,
ec.unmarshalInputSignUpInput,
ec.unmarshalInputUpdateMembershipInput,
ec.unmarshalInputUpdateOrganizationInput,
ec.unmarshalInputUpdatePersonalAPIKeyInput,
ec.unmarshalInputUpdateSAMLConfigurationInput,
@@ -2085,6 +2110,7 @@ type Mutation {
@session(required: PRESENT)
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload
@session(required: PRESENT)
updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload!
removeMember(input: RemoveMemberInput!): RemoveMemberPayload
@session(required: PRESENT)
@@ -2207,7 +2233,7 @@ type Membership implements Node {
id: ID!
createdAt: Datetime!
identity: Identity @goField(forceResolver: true)
profile: MembershipProfile @goField(forceResolver: true) @isViewer
profile: MembershipProfile @goField(forceResolver: true)
organization: Organization @goField(forceResolver: true)
role: MembershipRole!
permissions: [Permission!] @goField(forceResolver: true)
@@ -2576,6 +2602,12 @@ input InviteMemberInput {
fullName: String!
}
input UpdateMembershipInput {
organizationId: ID!
membershipId: ID!
role: MembershipRole!
}
input RemoveMemberInput {
organizationId: ID!
membershipId: ID!
@@ -2729,6 +2761,10 @@ type InviteMemberPayload {
invitationEdge: InvitationEdge!
}
type UpdateMembershipPayload {
membership: Membership!
}
type RemoveMemberPayload {
deletedMembershipId: ID!
}
@@ -3126,6 +3162,17 @@ func (ec *executionContext) field_Mutation_signUp_args(ctx context.Context, rawA
return args, nil
}
func (ec *executionContext) field_Mutation_updateMembership_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateMembershipInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipInput)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -5062,20 +5109,7 @@ func (ec *executionContext) _Membership_profile(ctx context.Context, field graph
func(ctx context.Context) (any, error) {
return ec.resolvers.Membership().Profile(ctx, obj)
},
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
directive0 := next
directive1 := func(ctx context.Context) (any, error) {
if ec.directives.IsViewer == nil {
var zeroVal *types.MembershipProfile
return zeroVal, errors.New("directive isViewer is not implemented")
}
return ec.directives.IsViewer(ctx, obj, directive0)
}
next = directive1
return next
},
nil,
ec.marshalOMembershipProfile2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembershipProfile,
true,
false,
@@ -6896,6 +6930,51 @@ func (ec *executionContext) fieldContext_Mutation_deleteInvitation(ctx context.C
return fc, nil
}
func (ec *executionContext) _Mutation_updateMembership(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Mutation_updateMembership,
func(ctx context.Context) (any, error) {
fc := graphql.GetFieldContext(ctx)
return ec.resolvers.Mutation().UpdateMembership(ctx, fc.Args["input"].(types.UpdateMembershipInput))
},
nil,
ec.marshalNUpdateMembershipPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipPayload,
true,
true,
)
}
func (ec *executionContext) fieldContext_Mutation_updateMembership(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "membership":
return ec.fieldContext_UpdateMembershipPayload_membership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UpdateMembershipPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_updateMembership_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_removeMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -10522,6 +10601,53 @@ func (ec *executionContext) fieldContext_SignUpPayload_identity(_ context.Contex
return fc, nil
}
func (ec *executionContext) _UpdateMembershipPayload_membership(ctx context.Context, field graphql.CollectedField, obj *types.UpdateMembershipPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_UpdateMembershipPayload_membership,
func(ctx context.Context) (any, error) {
return obj.Membership, nil
},
nil,
ec.marshalNMembership2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembership,
true,
true,
)
}
func (ec *executionContext) fieldContext_UpdateMembershipPayload_membership(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UpdateMembershipPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Membership_id(ctx, field)
case "createdAt":
return ec.fieldContext_Membership_createdAt(ctx, field)
case "identity":
return ec.fieldContext_Membership_identity(ctx, field)
case "profile":
return ec.fieldContext_Membership_profile(ctx, field)
case "organization":
return ec.fieldContext_Membership_organization(ctx, field)
case "role":
return ec.fieldContext_Membership_role(ctx, field)
case "permissions":
return ec.fieldContext_Membership_permissions(ctx, field)
case "lastSession":
return ec.fieldContext_Membership_lastSession(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Membership", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _UpdateOrganizationPayload_organization(ctx context.Context, field graphql.CollectedField, obj *types.UpdateOrganizationPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -13002,6 +13128,47 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj a
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateMembershipInput(ctx context.Context, obj any) (types.UpdateMembershipInput, error) {
var it types.UpdateMembershipInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"organizationId", "membershipId", "role"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "organizationId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId"))
data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.OrganizationID = data
case "membershipId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("membershipId"))
data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.MembershipID = data
case "role":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role"))
data, err := ec.unmarshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole(ctx, v)
if err != nil {
return it, err
}
it.Role = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Context, obj any) (types.UpdateOrganizationInput, error) {
var it types.UpdateOrganizationInput
asMap := map[string]any{}
@@ -14886,6 +15053,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_deleteInvitation(ctx, field)
})
case "updateMembership":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_updateMembership(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "removeMember":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_removeMember(ctx, field)
@@ -16634,6 +16808,45 @@ func (ec *executionContext) _SignUpPayload(ctx context.Context, sel ast.Selectio
return out
}
var updateMembershipPayloadImplementors = []string{"UpdateMembershipPayload"}
func (ec *executionContext) _UpdateMembershipPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateMembershipPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, updateMembershipPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UpdateMembershipPayload")
case "membership":
out.Values[i] = ec._UpdateMembershipPayload_membership(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var updateOrganizationPayloadImplementors = []string{"UpdateOrganizationPayload"}
func (ec *executionContext) _UpdateOrganizationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateOrganizationPayload) graphql.Marshaler {
@@ -18196,6 +18409,25 @@ func (ec *executionContext) marshalNTokenScope2ᚕgoᚗproboᚗincᚋproboᚋpkg
return ret
}
func (ec *executionContext) unmarshalNUpdateMembershipInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipInput(ctx context.Context, v any) (types.UpdateMembershipInput, error) {
res, err := ec.unmarshalInputUpdateMembershipInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNUpdateMembershipPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipPayload(ctx context.Context, sel ast.SelectionSet, v types.UpdateMembershipPayload) graphql.Marshaler {
return ec._UpdateMembershipPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNUpdateMembershipPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipPayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateMembershipPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._UpdateMembershipPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateOrganizationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateOrganizationInput(ctx context.Context, v any) (types.UpdateOrganizationInput, error) {
res, err := ec.unmarshalInputUpdateOrganizationInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -72,6 +72,7 @@ func NewMembership(membership *coredata.Membership) *Membership {
Profile: &MembershipProfile{
ID: membership.ID,
},
Role: membership.Role,
// Permissions: membership.Permissions,
// ProvisionedBy: membership.ProvisionedBy,
// Active: membership.Active,

View File

@@ -445,6 +445,16 @@ type SignUpPayload struct {
Identity *Identity `json:"identity,omitempty"`
}
type UpdateMembershipInput struct {
OrganizationID gid.GID `json:"organizationId"`
MembershipID gid.GID `json:"membershipId"`
Role coredata.MembershipRole `json:"role"`
}
type UpdateMembershipPayload struct {
Membership *Membership `json:"membership"`
}
type UpdateOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name *string `json:"name,omitempty"`

View File

@@ -302,6 +302,14 @@ func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *type
return nil, gqlutils.InternalServerError(ctx)
}
return &count, nil
case *organizationResolver:
count, err := r.iam.OrganizationService.CountMemberships(ctx, obj.ParentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count memberships", log.Error(err))
return nil, gqlutils.InternalServerError(ctx)
}
return &count, nil
}
@@ -864,6 +872,19 @@ func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.Del
return &types.DeleteInvitationPayload{DeletedInvitationID: input.InvitationID}, nil
}
// UpdateMembership is the resolver for the updateMembership field.
func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) {
membership, err := r.iam.OrganizationService.UpdateMempership(ctx, input.OrganizationID, input.MembershipID, input.Role)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err))
return nil, gqlutils.InternalServerError(ctx)
}
return &types.UpdateMembershipPayload{
Membership: types.NewMembership(membership),
}, nil
}
// RemoveMember is the resolver for the removeMember field.
func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) {
err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID)