From fbf670a99a52394c4f4a834d1818b5effe64a45a Mon Sep 17 00:00:00 2001 From: Gaurav Singh Date: Fri, 4 Oct 2024 15:01:44 +0530 Subject: [PATCH] feat (sdk): add member remove confirm dialog (#773) * feat (sdk): add member remove confirm dialog * chore: default state to false * fix: loading state for the modal * fix: change route based modal control * fix: loader and types --- .../members/MemberRemoveConfirm.tsx | 94 ++++++++++++++++ .../components/organization/members/index.tsx | 2 + .../organization/members/member.columns.tsx | 105 +++++++----------- .../organization/members/member.types.tsx | 11 +- .../react/components/organization/profile.tsx | 9 +- .../react/hooks/useOrganizationMembers.ts | 34 +++--- 6 files changed, 169 insertions(+), 86 deletions(-) create mode 100644 sdks/js/packages/core/react/components/organization/members/MemberRemoveConfirm.tsx diff --git a/sdks/js/packages/core/react/components/organization/members/MemberRemoveConfirm.tsx b/sdks/js/packages/core/react/components/organization/members/MemberRemoveConfirm.tsx new file mode 100644 index 000000000..0f3b9ddac --- /dev/null +++ b/sdks/js/packages/core/react/components/organization/members/MemberRemoveConfirm.tsx @@ -0,0 +1,94 @@ +import { + Flex, + Text, + Dialog, + Button, + Separator, + Image +} from '@raystack/apsara'; +import cross from '~/react/assets/cross.svg'; +import { useNavigate, useParams } from '@tanstack/react-router'; +import { useFrontier } from '~/react/contexts/FrontierContext'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +const MemberRemoveConfirm = () => { + const navigate = useNavigate({ from: '/members/remove-member/$memberId/$invited' }); + const { memberId, invited } = useParams({ from: '/members/remove-member/$memberId/$invited' }); + const { client, activeOrganization } = useFrontier(); + const organizationId = activeOrganization?.id ?? '' + const [isLoading, setIsLoading] = useState(false); + + const deleteMember = async () => { + setIsLoading(true); + try { + if (invited === 'true') { + await client?.frontierServiceDeleteOrganizationInvitation( + organizationId, + memberId as string + ); + } else { + await client?.frontierServiceRemoveOrganizationUser( + organizationId, + memberId as string + ); + } + navigate({ to: '/members' }); + toast.success('Member deleted'); + } catch ({ error }: any) { + toast.error('Something went wrong', { + description: error.message + }); + } finally { + setIsLoading(false); + } + }; + + return ( + navigate({ to: '/members' })}> + + + + Remove member? + + cross isLoading ? null : navigate({ to: '/members' })} + style={{ cursor: isLoading ? 'not-allowed' : 'pointer' }} + data-test-id="close-remove-member-dialog" + /> + + + + + Are you sure you want to remove this member from the organization? + + + + + + + + + + ) +} + +export default MemberRemoveConfirm \ No newline at end of file diff --git a/sdks/js/packages/core/react/components/organization/members/index.tsx b/sdks/js/packages/core/react/components/organization/members/index.tsx index 9282469b7..5036fc2ec 100644 --- a/sdks/js/packages/core/react/components/organization/members/index.tsx +++ b/sdks/js/packages/core/react/components/organization/members/index.tsx @@ -151,6 +151,7 @@ const MembersTable = ({ emptyState={noDataChildren} parentStyle={{ height: 'calc(100vh - 222px)' }} style={tableStyle} + > Invite people diff --git a/sdks/js/packages/core/react/components/organization/members/member.columns.tsx b/sdks/js/packages/core/react/components/organization/members/member.columns.tsx index b1c92c6a4..d8765668e 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.columns.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.columns.tsx @@ -9,30 +9,28 @@ import { DropdownMenu, Flex, Label, - Text + Text, } from '@raystack/apsara'; import { useNavigate } from '@tanstack/react-router'; import { toast } from 'sonner'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { - V1Beta1Invitation, V1Beta1Policy, V1Beta1Role, - V1Beta1User } from '~/src'; -import { Role } from '~/src/types'; import { differenceWith, getInitials, isEqualById } from '~/utils'; import styles from '../organization.module.css'; +import { MemberWithInvite } from '~/react/hooks/useOrganizationMembers'; + + export const getColumns = ( organizationId: string, - memberRoles: Record = {}, - roles: Role[] = [], + memberRoles: Record = {}, + roles: V1Beta1Role[] = [], canDeleteUser = false, - refetch = () => null -): ApsaraColumnDef< - V1Beta1User & V1Beta1Invitation & { invited?: boolean } ->[] => [ + refetch = () => {}, +): ApsaraColumnDef[] => [ { header: '', accessorKey: 'avatar', @@ -105,7 +103,7 @@ export const getColumns = ( cell: ({ row }) => ( ( @@ -127,7 +125,7 @@ const MembersActions = ({ excludedRoles = [], refetch = () => null }: { - member: V1Beta1User; + member: MemberWithInvite; canUpdateGroup?: boolean; organizationId: string; excludedRoles: V1Beta1Role[]; @@ -136,29 +134,6 @@ const MembersActions = ({ const { client } = useFrontier(); const navigate = useNavigate({ from: '/members' }); - async function deleteMember() { - try { - // @ts-ignore - if (member?.invited) { - await client?.frontierServiceDeleteOrganizationInvitation( - // @ts-ignore - member.org_id, - member?.id as string - ); - } else { - await client?.frontierServiceRemoveOrganizationUser( - organizationId, - member?.id as string - ); - } - navigate({ to: '/members' }); - toast.success('Member deleted'); - } catch ({ error }: any) { - toast.error('Something went wrong', { - description: error.message - }); - } - } async function updateRole(role: V1Beta1Role) { try { const resource = `app/organization:${organizationId}`; @@ -191,37 +166,43 @@ const MembersActions = ({ } return canUpdateGroup ? ( - - - - - - - {excludedRoles.map((role: V1Beta1Role) => ( - + <> + + + + + + + {excludedRoles.map((role: V1Beta1Role) => ( + +
updateRole(role)} + className={styles.dropdownActionItem} + data-test-id={`update-role-${role?.name}-dropdown-item`} + > + + Make {role.title} +
+
+ ))} + +
updateRole(role)} + onClick={() => navigate({ to: `/members/remove-member/$memberId/$invited`, params: { + memberId: member?.id || "", + invited: (member?.invited || false).toString() + } + })} className={styles.dropdownActionItem} - data-test-id={`update-role-${role?.name}-dropdown-item`} + data-test-id="remove-member-dropdown-item" > - - Make {role.title} + + Remove
- ))} - - -
- - Remove -
-
-
-
-
+
+
+
+ ) : null; }; diff --git a/sdks/js/packages/core/react/components/organization/members/member.types.tsx b/sdks/js/packages/core/react/components/organization/members/member.types.tsx index 03c446289..7353bb752 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.types.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.types.tsx @@ -1,7 +1,8 @@ -import { Role, User } from '~/src/types'; +import { MemberWithInvite } from '~/react/hooks/useOrganizationMembers'; +import { V1Beta1User, V1Beta1Role } from '~/src'; export type MembersType = { - users: User[]; + users: V1Beta1User[]; }; export enum MemberActionmethods { @@ -10,11 +11,11 @@ export enum MemberActionmethods { export type MembersTableType = { isLoading?: boolean; - users: User[]; + users: MemberWithInvite[]; organizationId: string; canCreateInvite?: boolean; canDeleteUser?: boolean; - memberRoles: Record; - roles: Role[]; + memberRoles: Record; + roles: V1Beta1Role[]; refetch?: () => void; }; diff --git a/sdks/js/packages/core/react/components/organization/profile.tsx b/sdks/js/packages/core/react/components/organization/profile.tsx index fb0b05096..b2f714355 100644 --- a/sdks/js/packages/core/react/components/organization/profile.tsx +++ b/sdks/js/packages/core/react/components/organization/profile.tsx @@ -40,6 +40,7 @@ import Tokens from './tokens'; import { ConfirmCycleSwitch } from './billing/cycle-switch'; import Plans from './plans'; import ConfirmPlanChange from './plans/confirm-change'; +import MemberRemoveConfirm from './members/MemberRemoveConfirm'; interface OrganizationProfileProps { organizationId: string; @@ -139,6 +140,12 @@ const inviteMemberRoute = createRoute({ component: InviteMember }); +const removeMemberRoute = createRoute({ + getParentRoute: () => membersRoute, + path: '/remove-member/$memberId/$invited', + component: MemberRemoveConfirm +}); + const teamsRoute = createRoute({ getParentRoute: () => rootRoute, path: '/teams', @@ -262,7 +269,7 @@ const tokensRoute = createRoute({ const routeTree = rootRoute.addChildren([ indexRoute.addChildren([deleteOrgRoute]), securityRoute, - membersRoute.addChildren([inviteMemberRoute]), + membersRoute.addChildren([inviteMemberRoute, removeMemberRoute]), teamsRoute.addChildren([addTeamRoute]), domainsRoute.addChildren([ addDomainRoute, diff --git a/sdks/js/packages/core/react/hooks/useOrganizationMembers.ts b/sdks/js/packages/core/react/hooks/useOrganizationMembers.ts index acb14a6bc..de7f9f637 100644 --- a/sdks/js/packages/core/react/hooks/useOrganizationMembers.ts +++ b/sdks/js/packages/core/react/hooks/useOrganizationMembers.ts @@ -1,18 +1,22 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { V1Beta1User } from '~/src'; -import { Role } from '~/src/types'; +import { useCallback, useEffect, useState } from 'react'; +import { V1Beta1User, V1Beta1Role, V1Beta1Invitation } from '~/src'; import { PERMISSIONS } from '~/utils'; import { useFrontier } from '../contexts/FrontierContext'; + +export type MemberWithInvite = V1Beta1User & V1Beta1Invitation & {invited?: boolean} + + + export const useOrganizationMembers = ({ showInvitations = false }) => { - const [users, setUsers] = useState([]); - const [roles, setRoles] = useState([]); - const [invitations, setInvitations] = useState([]); + const [users, setUsers] = useState([]); + const [roles, setRoles] = useState([]); + const [invitations, setInvitations] = useState([]); const [isUsersLoading, setIsUsersLoading] = useState(false); const [isRolesLoading, setIsRolesLoading] = useState(false); const [isInvitationsLoading, setIsInvitationsLoading] = useState(false); - const [memberRoles, setMemberRoles] = useState>({}); + const [memberRoles, setMemberRoles] = useState>({}); const { client, activeOrganization: organization } = useFrontier(); @@ -24,7 +28,7 @@ export const useOrganizationMembers = ({ showInvitations = false }) => { // @ts-ignore data: { users, role_pairs } } = await client?.frontierServiceListOrganizationUsers(organization?.id, { - withRoles: true + with_roles: true }); setUsers(users); setMemberRoles( @@ -69,7 +73,7 @@ export const useOrganizationMembers = ({ showInvitations = false }) => { } = await client?.frontierServiceListOrganizationInvitations( organization?.id ); - const invitedUsers = invitations.map((user: V1Beta1User) => ({ + const invitedUsers : MemberWithInvite[] = invitations.map((user: V1Beta1User) => ({ ...user, invited: true })); @@ -95,16 +99,10 @@ export const useOrganizationMembers = ({ showInvitations = false }) => { } }, [showInvitations, fetchInvitations]); - const isFetching = isUsersLoading || isInvitationsLoading; + const isFetching = isUsersLoading || isInvitationsLoading || isRolesLoading; + - const updatedUsers = useMemo(() => { - const totalUsers = [...users, ...invitations]; - return isFetching - ? ([{ id: 1 }, { id: 2 }, { id: 3 }] as any) - : totalUsers.length - ? totalUsers - : []; - }, [invitations, isFetching, users]); + const updatedUsers = [...users, ...invitations]; const refetch = useCallback(() => { fetchOrganizationUser();