Skip to content

Commit

Permalink
feat (sdk): add member remove confirm dialog (#773)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
paanSinghCoder authored Oct 4, 2024
1 parent bbebf12 commit fbf670a
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={true} onOpenChange={() => navigate({ to: '/members' })}>
<Dialog.Content style={{ padding: 0, maxWidth: '400px', width: '100%', zIndex: '60' }}>
<Flex justify="between" style={{ padding: '16px 24px' }}>
<Text size={6} style={{ fontWeight: '500' }}>
Remove member?
</Text>
<Image
alt="cross"
src={cross}
onClick={() => isLoading ? null : navigate({ to: '/members' })}
style={{ cursor: isLoading ? 'not-allowed' : 'pointer' }}
data-test-id="close-remove-member-dialog"
/>
</Flex>
<Separator />
<Flex direction="column" gap="medium" style={{ padding: '24px' }}>
<Text size={4}>
Are you sure you want to remove this member from the organization?
</Text>
</Flex>
<Separator />
<Flex justify="end" style={{ padding: 'var(--pd-16)' }} gap="medium">
<Button
size="medium"
variant="secondary"
onClick={() => navigate({ to: '/members' })}
data-test-id="cancel-remove-member-dialog"
disabled={isLoading}
>
Cancel
</Button>
<Button
size="medium"
variant="danger"
onClick={deleteMember}
data-test-id="confirm-remove-member-dialog"
disabled={isLoading}
>
{isLoading ? 'Removing...' : 'Remove'}
</Button>
</Flex>
</Dialog.Content>
</Dialog>
)
}

export default MemberRemoveConfirm
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const MembersTable = ({
emptyState={noDataChildren}
parentStyle={{ height: 'calc(100vh - 222px)' }}
style={tableStyle}

>
<DataTable.Toolbar
style={{ padding: 0, border: 0, marginBottom: 'var(--pd-16)' }}
Expand Down Expand Up @@ -180,6 +181,7 @@ const MembersTable = ({
})
}
disabled={!canCreateInvite}
data-test-id="frontier-sdk-remove-member-link"
>
Invite people
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Role[]> = {},
roles: Role[] = [],
memberRoles: Record<string, V1Beta1Role[]> = {},
roles: V1Beta1Role[] = [],
canDeleteUser = false,
refetch = () => null
): ApsaraColumnDef<
V1Beta1User & V1Beta1Invitation & { invited?: boolean }
>[] => [
refetch = () => {},
): ApsaraColumnDef<MemberWithInvite>[] => [
{
header: '',
accessorKey: 'avatar',
Expand Down Expand Up @@ -105,7 +103,7 @@ export const getColumns = (
cell: ({ row }) => (
<MembersActions
refetch={refetch}
member={row.original as V1Beta1User}
member={row.original}
organizationId={organizationId}
canUpdateGroup={canDeleteUser}
excludedRoles={differenceWith<V1Beta1Role>(
Expand All @@ -127,7 +125,7 @@ const MembersActions = ({
excludedRoles = [],
refetch = () => null
}: {
member: V1Beta1User;
member: MemberWithInvite;
canUpdateGroup?: boolean;
organizationId: string;
excludedRoles: V1Beta1Role[];
Expand All @@ -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}`;
Expand Down Expand Up @@ -191,37 +166,43 @@ const MembersActions = ({
}

return canUpdateGroup ? (
<DropdownMenu style={{ padding: '0 !important' }}>
<DropdownMenu.Trigger asChild style={{ cursor: 'pointer' }}>
<DotsHorizontalIcon />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Group style={{ padding: 0 }}>
{excludedRoles.map((role: V1Beta1Role) => (
<DropdownMenu.Item style={{ padding: 0 }} key={role.id}>
<>
<DropdownMenu style={{ padding: '0 !important' }}>
<DropdownMenu.Trigger asChild style={{ cursor: 'pointer' }}>
<DotsHorizontalIcon />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Group style={{ padding: 0 }}>
{excludedRoles.map((role: V1Beta1Role) => (
<DropdownMenu.Item style={{ padding: 0 }} key={role.id}>
<div
onClick={() => updateRole(role)}
className={styles.dropdownActionItem}
data-test-id={`update-role-${role?.name}-dropdown-item`}
>
<UpdateIcon />
Make {role.title}
</div>
</DropdownMenu.Item>
))}

<DropdownMenu.Item style={{ padding: 0 }}>
<div
onClick={() => 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"
>
<UpdateIcon />
Make {role.title}
<TrashIcon />
Remove
</div>
</DropdownMenu.Item>
))}

<DropdownMenu.Item style={{ padding: 0 }}>
<div
onClick={deleteMember}
className={styles.dropdownActionItem}
data-test-id="remove-member-dropdown-item"
>
<TrashIcon />
Remove
</div>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu>
</>
) : null;
};
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,11 +11,11 @@ export enum MemberActionmethods {

export type MembersTableType = {
isLoading?: boolean;
users: User[];
users: MemberWithInvite[];
organizationId: string;
canCreateInvite?: boolean;
canDeleteUser?: boolean;
memberRoles: Record<string, Role[]>;
roles: Role[];
memberRoles: Record<string, V1Beta1Role[]>;
roles: V1Beta1Role[];
refetch?: () => void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 16 additions & 18 deletions sdks/js/packages/core/react/hooks/useOrganizationMembers.ts
Original file line number Diff line number Diff line change
@@ -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<V1Beta1User[]>([]);
const [roles, setRoles] = useState<V1Beta1Role[]>([]);
const [invitations, setInvitations] = useState<MemberWithInvite[]>([]);

const [isUsersLoading, setIsUsersLoading] = useState(false);
const [isRolesLoading, setIsRolesLoading] = useState(false);
const [isInvitationsLoading, setIsInvitationsLoading] = useState(false);
const [memberRoles, setMemberRoles] = useState<Record<string, Role[]>>({});
const [memberRoles, setMemberRoles] = useState<Record<string, V1Beta1Role[]>>({});

const { client, activeOrganization: organization } = useFrontier();

Expand All @@ -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(
Expand Down Expand Up @@ -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
}));
Expand All @@ -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();
Expand Down

0 comments on commit fbf670a

Please sign in to comment.