diff --git a/web/sdk/react/views-new/teams/components/add-member-menu.tsx b/web/sdk/react/views-new/teams/components/add-member-menu.tsx index 2728c204d..66b88aabf 100644 --- a/web/sdk/react/views-new/teams/components/add-member-menu.tsx +++ b/web/sdk/react/views-new/teams/components/add-member-menu.tsx @@ -14,19 +14,22 @@ import { useQuery, useMutation } from '@connectrpc/connect-query'; import { FrontierServiceQueries, ListOrganizationUsersRequestSchema, - AddGroupUsersRequestSchema, + SetGroupMemberRoleRequestSchema, + type Role as ProtoRole, type User } from '@raystack/proton/frontier'; import { create } from '@bufbuild/protobuf'; import { useFrontier } from '../../../contexts/FrontierContext'; import { AuthTooltipMessage } from '../../../utils'; -import { filterUsersfromUsers, getInitials } from '../../../../utils'; +import { PERMISSIONS, filterUsersfromUsers, getInitials } from '../../../../utils'; +import { handleConnectError } from '../../../../utils/error'; import styles from '../team-details-view.module.css'; interface AddMemberMenuProps { teamId: string; canUpdateGroup: boolean; members: User[]; + roles: ProtoRole[]; refetch: () => void; } @@ -34,6 +37,7 @@ export function AddMemberMenu({ teamId, canUpdateGroup, members, + roles, refetch }: AddMemberMenuProps) { const { activeOrganization: organization } = useFrontier(); @@ -70,8 +74,13 @@ export function AddMemberMenu({ [orgUsers, members] ); - const { mutate: addGroupUsers } = useMutation( - FrontierServiceQueries.addGroupUsers, + const memberRoleId = useMemo( + () => roles.find(r => r.name === PERMISSIONS.RoleGroupMember)?.id ?? '', + [roles] + ); + + const { mutate: setGroupMemberRole } = useMutation( + FrontierServiceQueries.setGroupMemberRole, { onSuccess: () => { toastManager.add({ @@ -81,10 +90,29 @@ export function AddMemberMenu({ refetch(); }, onError: (err: Error) => { - toastManager.add({ - title: 'Something went wrong', - description: err.message, - type: 'error' + handleConnectError(err, { + AlreadyExists: () => + toastManager.add({ + title: 'Member already exists in this team', + type: 'error' + }), + PermissionDenied: () => + toastManager.add({ + title: "You don't have permission to perform this action", + type: 'error' + }), + InvalidArgument: (e) => + toastManager.add({ + title: 'Invalid input', + description: e.message, + type: 'error' + }), + Default: (e) => + toastManager.add({ + title: 'Something went wrong', + description: e.message, + type: 'error' + }) }); } } @@ -92,16 +120,18 @@ export function AddMemberMenu({ const addMember = useCallback( (userId: string) => { - if (!userId || !organization?.id || !teamId) return; - addGroupUsers( - create(AddGroupUsersRequestSchema, { - id: teamId, + if (!userId || !organization?.id || !teamId || !memberRoleId) return; + setGroupMemberRole( + create(SetGroupMemberRoleRequestSchema, { + groupId: teamId, orgId: organization.id, - userIds: [userId] + principalId: userId, + principalType: PERMISSIONS.UserPrincipal, + roleId: memberRoleId }) ); }, - [addGroupUsers, organization?.id, teamId] + [setGroupMemberRole, organization?.id, teamId, memberRoleId] ); if (!canUpdateGroup) { diff --git a/web/sdk/react/views-new/teams/team-details-view.tsx b/web/sdk/react/views-new/teams/team-details-view.tsx index 31703f1f7..984a3e38f 100644 --- a/web/sdk/react/views-new/teams/team-details-view.tsx +++ b/web/sdk/react/views-new/teams/team-details-view.tsx @@ -21,27 +21,20 @@ import { } from '@raystack/apsara-v1'; import deleteIcon from '../../assets/delete.svg'; import { toastManager } from '@raystack/apsara-v1'; -import { - useQuery, - useMutation, - createConnectQueryKey, - useTransport -} from '@connectrpc/connect-query'; -import { useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation } from '@connectrpc/connect-query'; import { FrontierServiceQueries, GetGroupRequestSchema, ListGroupUsersRequestSchema, ListRolesRequestSchema, - CreatePolicyRequestSchema, - DeletePolicyRequestSchema, - ListPoliciesRequestSchema, + SetGroupMemberRoleRequestSchema, type Role as ProtoRole } from '@raystack/proton/frontier'; import { create } from '@bufbuild/protobuf'; import { useFrontier } from '../../contexts/FrontierContext'; import { usePermissions } from '../../hooks/usePermissions'; import { PERMISSIONS, shouldShowComponent } from '../../../utils'; +import { handleConnectError } from '../../../utils/error'; import { ViewContainer } from '../../components/view-container'; import { ViewHeader } from '../../components/view-header'; import { @@ -209,53 +202,21 @@ export function TeamDetailsView({ [memberRoles, roles, canUpdateGroup] ); - const queryClient = useQueryClient(); - const transport = useTransport(); - - const { mutateAsync: deletePolicy } = useMutation( - FrontierServiceQueries.deletePolicy - ); - const { mutateAsync: createPolicy } = useMutation( - FrontierServiceQueries.createPolicy + const { mutateAsync: setGroupMemberRole } = useMutation( + FrontierServiceQueries.setGroupMemberRole ); const updateMemberRole = useCallback( async (memberId: string, role: ProtoRole) => { + if (!organization?.id) return; try { - const principal = `${PERMISSIONS.UserNamespace}:${memberId}`; - - const input = create(ListPoliciesRequestSchema, { - groupId: teamId, - userId: memberId - }); - - const policiesData = await queryClient.fetchQuery({ - queryKey: createConnectQueryKey({ - schema: FrontierServiceQueries.listPolicies, - transport, - input, - cardinality: 'finite' - }) - }); - - const policies = - (policiesData as { policies?: { id?: string }[] })?.policies ?? []; - - await Promise.all( - policies.map(p => - deletePolicy( - create(DeletePolicyRequestSchema, { id: p.id || '' }) - ) - ) - ); - - await createPolicy( - create(CreatePolicyRequestSchema, { - body: { - roleId: role.id as string, - resource: `app/group:${teamId}`, - principal - } + await setGroupMemberRole( + create(SetGroupMemberRoleRequestSchema, { + groupId: teamId, + orgId: organization.id, + principalId: memberId, + principalType: PERMISSIONS.UserPrincipal, + roleId: role.id as string }) ); refetchMembers(); @@ -264,17 +225,33 @@ export function TeamDetailsView({ type: 'success' }); } catch (error) { - toastManager.add({ - title: 'Something went wrong', - description: - error instanceof Error - ? error.message - : 'Failed to update member role', - type: 'error' + handleConnectError(error, { + AlreadyExists: () => + toastManager.add({ + title: 'Member already exists in this team', + type: 'error' + }), + PermissionDenied: () => + toastManager.add({ + title: "You don't have permission to perform this action", + type: 'error' + }), + InvalidArgument: (e) => + toastManager.add({ + title: 'Invalid input', + description: e.message, + type: 'error' + }), + Default: (e) => + toastManager.add({ + title: 'Something went wrong', + description: e.message, + type: 'error' + }) }); } }, - [queryClient, transport, deletePolicy, createPolicy, teamId, refetchMembers] + [setGroupMemberRole, teamId, organization?.id, refetchMembers] ); const handleDeleteSuccess = useCallback(() => { @@ -348,6 +325,7 @@ export function TeamDetailsView({ teamId={teamId} canUpdateGroup={canUpdateGroup} members={members} + roles={roles} refetch={refetchMembers} /> )} diff --git a/web/sdk/react/views/teams/details/invite-team-member-dialog.tsx b/web/sdk/react/views/teams/details/invite-team-member-dialog.tsx index 9690c8bf0..ca46c939a 100644 --- a/web/sdk/react/views/teams/details/invite-team-member-dialog.tsx +++ b/web/sdk/react/views/teams/details/invite-team-member-dialog.tsx @@ -12,7 +12,7 @@ import { Select, Label } from '@raystack/apsara'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import * as yup from 'yup'; import { useFrontier } from '~/react/contexts/FrontierContext'; @@ -22,8 +22,7 @@ import { handleSelectValueChange } from '~/react/utils'; import { useQuery, useMutation } from '@connectrpc/connect-query'; import { FrontierServiceQueries, - CreatePolicyRequestSchema, - AddGroupUsersRequestSchema, + SetGroupMemberRoleRequestSchema, ListOrganizationUsersRequestSchema, ListGroupUsersRequestSchema, ListOrganizationRolesRequestSchema, @@ -168,49 +167,26 @@ export const InviteTeamMemberDialog = ({ } }, [rolesError]); - // Create policy using Connect RPC - const createPolicyMutation = useMutation( - FrontierServiceQueries.createPolicy, - ); - - const addGroupTeamPolicy = useCallback( - async (roleId: string, userId: string) => { - const role = roles.find(r => r.id === roleId); - if (role?.name && role.name !== PERMISSIONS.RoleGroupMember) { - const resource = `${PERMISSIONS.GroupPrincipal}:${teamId}`; - const principal = `${PERMISSIONS.UserPrincipal}:${userId}`; - - const request = create(CreatePolicyRequestSchema, { - body: { - roleId: roleId, - resource, - principal - } - }); - - await createPolicyMutation.mutateAsync(request); - } - }, - [roles, teamId, createPolicyMutation] - ); - - // Add group users using Connect RPC - const addGroupUsersMutation = useMutation( - FrontierServiceQueries.addGroupUsers, + // Single upsert mutation: SetGroupMemberRole adds the user with the + // chosen role in one call, replacing the previous AddGroupUsers + + // CreatePolicy two-step workaround. + const setGroupMemberRoleMutation = useMutation( + FrontierServiceQueries.setGroupMemberRole, ); async function onSubmit({ role, userId }: InviteSchemaType) { if (!userId || !role || !organization?.id) return; - const request = create(AddGroupUsersRequestSchema, { - id: teamId as string, + const request = create(SetGroupMemberRoleRequestSchema, { + groupId: teamId as string, orgId: organization.id, - userIds: [userId] + principalId: userId, + principalType: PERMISSIONS.UserPrincipal, + roleId: role }); try { - await addGroupUsersMutation.mutateAsync(request); - await addGroupTeamPolicy(role, userId); + await setGroupMemberRoleMutation.mutateAsync(request); toast.success('member added'); handleOpenChange(false); } catch (error) { diff --git a/web/sdk/react/views/teams/details/team-member-columns.tsx b/web/sdk/react/views/teams/details/team-member-columns.tsx index 27743e3d6..09ec44ac5 100644 --- a/web/sdk/react/views/teams/details/team-member-columns.tsx +++ b/web/sdk/react/views/teams/details/team-member-columns.tsx @@ -15,17 +15,14 @@ import { type DataTableColumnDef, getAvatarColor } from '@raystack/apsara'; -import { differenceWith, getInitials, isEqualById } from '~/utils'; -import { useMutation, useQuery } from '@connectrpc/connect-query'; +import { PERMISSIONS, differenceWith, getInitials, isEqualById } from '~/utils'; +import { useMutation } from '@connectrpc/connect-query'; import { FrontierServiceQueries, RemoveGroupUserRequestSchema, - DeletePolicyRequestSchema, - CreatePolicyRequestSchema, - Policy, + SetGroupMemberRoleRequestSchema, Role, - User, - ListPoliciesRequestSchema + User } from '@raystack/proton/frontier'; import { create } from '@bufbuild/protobuf'; @@ -161,31 +158,9 @@ const MembersActions = ({ removeGroupUserMutation.mutate(request); } - // Get policies using Connect RPC - const { refetch: refetchPolicies } = useQuery( - FrontierServiceQueries.listPolicies, - create(ListPoliciesRequestSchema, { - groupId: teamId, - userId: member?.id as string - }), - { enabled: false } // Only fetch when needed - ); - - // Delete policy using Connect RPC - const deletePolicyMutation = useMutation( - FrontierServiceQueries.deletePolicy, - { - onError: error => { - toast.error('Something went wrong', { - description: error.message - }); - } - } - ); - - // Create policy using Connect RPC - const createPolicyMutation = useMutation( - FrontierServiceQueries.createPolicy, + // Upsert the member role via SetGroupMemberRole RPC. + const setGroupMemberRoleMutation = useMutation( + FrontierServiceQueries.setGroupMemberRole, { onSuccess: () => { refetch(); @@ -201,34 +176,15 @@ const MembersActions = ({ async function updateRole(role: Role) { try { - const resource = `app/group:${teamId}`; - const principal = `app/user:${member?.id}`; - - // Get policies using Connect RPC - const policiesResponse = await refetchPolicies(); - const policies = policiesResponse?.data?.policies || []; - - // Delete existing policies - const deletePromises = policies.map((p: Policy) => { - const deleteRequest = create(DeletePolicyRequestSchema, { - id: p.id as string - }); - return deletePolicyMutation.mutateAsync(deleteRequest); - }); - - await Promise.all(deletePromises); - - // Create new policy - const createRequest = create(CreatePolicyRequestSchema, { - body: { - roleId: role.id as string, - title: role.name as string, - resource: resource, - principal: principal - } + const request = create(SetGroupMemberRoleRequestSchema, { + groupId: teamId, + orgId: organizationId, + principalId: member?.id as string, + principalType: PERMISSIONS.UserPrincipal, + roleId: role.id as string }); - await createPolicyMutation.mutateAsync(createRequest); + await setGroupMemberRoleMutation.mutateAsync(request); } catch (error: any) { toast.error('Something went wrong', { description: error?.message diff --git a/web/sdk/react/views/teams/details/team-members.tsx b/web/sdk/react/views/teams/details/team-members.tsx index 59670ded8..f8a708703 100644 --- a/web/sdk/react/views/teams/details/team-members.tsx +++ b/web/sdk/react/views/teams/details/team-members.tsx @@ -32,7 +32,7 @@ import { getColumns } from './team-member-columns'; import { useQuery, useMutation } from '@connectrpc/connect-query'; import { FrontierServiceQueries, - AddGroupUsersRequestSchema, + SetGroupMemberRoleRequestSchema, ListOrganizationUsersRequestSchema, type User, type Role @@ -133,6 +133,7 @@ export const TeamMembers = ({ canUpdateGroup={canUpdateGroup} refetchMembers={refetchMembers} members={members} + roles={roles} teamId={teamId} onInviteClick={onInviteClick} /> @@ -156,6 +157,7 @@ interface AddMemberDropdownProps { canUpdateGroup: boolean; refetchMembers: () => void; members: User[]; + roles: Role[]; teamId: string; onInviteClick?: () => void; } @@ -164,6 +166,7 @@ const AddMemberDropdown = ({ canUpdateGroup, refetchMembers, members, + roles, teamId, onInviteClick }: AddMemberDropdownProps) => { @@ -217,9 +220,14 @@ const AddMemberDropdown = ({ setQuery(e.target.value); } - // Add group user using Connect RPC - const addGroupUserMutation = useMutation( - FrontierServiceQueries.addGroupUsers, + const memberRoleId = useMemo( + () => roles.find(r => r.name === PERMISSIONS.RoleGroupMember)?.id ?? '', + [roles] + ); + + // Add group user as member using Connect RPC + const setGroupMemberRoleMutation = useMutation( + FrontierServiceQueries.setGroupMemberRole, { onSuccess: () => { toast.success('member added'); @@ -237,17 +245,19 @@ const AddMemberDropdown = ({ const addMember = useCallback( (userId: string) => { - if (!userId || !organization?.id) return; + if (!userId || !organization?.id || !memberRoleId) return; - const request = create(AddGroupUsersRequestSchema, { - id: teamId, + const request = create(SetGroupMemberRoleRequestSchema, { + groupId: teamId, orgId: organization.id, - userIds: [userId] + principalId: userId, + principalType: PERMISSIONS.UserPrincipal, + roleId: memberRoleId }); - addGroupUserMutation.mutate(request); + setGroupMemberRoleMutation.mutate(request); }, - [organization?.id, teamId, addGroupUserMutation] + [organization?.id, teamId, memberRoleId, setGroupMemberRoleMutation] ); return (