Skip to content

Commit

Permalink
feat: delete invitation to org (#749)
Browse files Browse the repository at this point in the history
* feat: delete invitation to org

* chore(org-invite): converted callback to functional component

* chore(org-invite): remove action button for used/accepted emails

* fix: don't allow deleting used invites on server

---------

Co-authored-by: BlankParticle <blankparticle@gmail.com>
  • Loading branch information
SySagar and BlankParticle committed Aug 28, 2024
1 parent ea9934c commit a3b50fd
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 0 deletions.
84 changes: 84 additions & 0 deletions apps/platform/trpc/routers/orgRouter/users/invitesRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,90 @@ export const invitesRouter = router({
};
}),

deleteInvite: orgAdminProcedure
.input(
z.object({
invitePublicId: typeIdValidator('orgInvitations'),
orgMemberPublicId: typeIdValidator('orgMembers'),
emailIdentitiesPublicId: typeIdValidator('emailIdentities').optional()
})
)
.mutation(async ({ ctx, input }) => {
const { db } = ctx;
const { orgMemberPublicId } = input;

const orgMember = await db.query.orgMembers.findFirst({
where: eq(orgMembers.publicId, orgMemberPublicId),
columns: {
id: true,
orgMemberProfileId: true,
personalSpaceId: true
}
});

if (!orgMember) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Org Member not found'
});
}

const orgInvitesResponse = await db.query.orgInvitations.findFirst({
where: eq(orgInvitations.orgMemberId, orgMember.id),
columns: {
id: true,
acceptedAt: true
}
});

if (!orgInvitesResponse) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Invitation not found'
});
}

if (orgInvitesResponse.acceptedAt) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Used invitation cannot be deleted'
});
}

const {
id: orgMemberId,
orgMemberProfileId,
personalSpaceId
} = orgMember;

await db.transaction(async (db) => {
if (input.emailIdentitiesPublicId) {
await db
.delete(emailIdentities)
.where(eq(emailIdentities.publicId, input.emailIdentitiesPublicId));
}

if (personalSpaceId) {
await db
.delete(spaceMembers)
.where(eq(spaceMembers.spaceId, personalSpaceId));
await db.delete(spaces).where(eq(spaces.id, personalSpaceId));
}

await db
.delete(orgMemberProfiles)
.where(eq(orgMemberProfiles.id, orgMemberProfileId));

await db.delete(orgMembers).where(eq(orgMembers.id, orgMemberId));

if (orgInvitesResponse) {
await db
.delete(orgInvitations)
.where(eq(orgInvitations.id, orgInvitesResponse.id));
}
});
}),

validateInvite: publicProcedure
.use(ratelimiter({ limit: 10, namespace: 'invite.validate' }))
.input(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
'use client';

import { createColumnHelper, type ColumnDef } from '@tanstack/react-table';
import { Button } from '@/src/components/shadcn-ui/button';
import { CopyButton } from '@/src/components/copy-button';
import { Badge } from '@/src/components/shadcn-ui/badge';
import { useOrgShortcode } from '@/src/hooks/use-params';
import type { RouterOutputs } from '@/src/lib/trpc';
import { Avatar } from '@/src/components/avatar';
import { Trash } from '@phosphor-icons/react';
import { platform } from '@/src/lib/trpc';
import { cn } from '@/src/lib/utils';
import { format } from 'date-fns';
import { env } from '@/src/env';
import { toast } from 'sonner';

type Member =
RouterOutputs['org']['users']['invites']['viewInvites']['invites'][number];
Expand Down Expand Up @@ -129,5 +135,50 @@ export const columns: ColumnDef<Member>[] = [
</div>
) : null;
}
}),
columnHelper.display({
id: 'delete',
header: '',

cell: ({ row }) => <DeleteInviteCell row={row.original} />
})
];

const DeleteInviteCell: React.FC<{ row: Member }> = ({ row }) => {
const orgShortcode = useOrgShortcode();
const utils = platform.useUtils();

const { mutate: deleteInvite } =
platform.org.users.invites.deleteInvite.useMutation({
onSuccess: () => {
toast.success('Invitation deleted');
void utils.org.users.invites.viewInvites.refetch();
}
});

const handleDelete = () => {
const invitePublicId = row.publicId as string;
const orgMemberPublicId = row.orgMember?.publicId as string;

deleteInvite({
orgShortcode,
invitePublicId,
orgMemberPublicId
});
};

return (
<div className="flex h-full w-full items-center">
<Button
onClick={handleDelete}
variant={'ghost'}
size="icon"
className={cn(
'bg-red-4 hover:bg-red-5 m-1 uppercase',
row.acceptedAt ? 'hidden' : 'flex'
)}>
<Trash className="fill-white" />
</Button>
</div>
);
};

0 comments on commit a3b50fd

Please sign in to comment.