Skip to content

Commit cf2fceb

Browse files
feat: delete invitation to org (#749)
* 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 <[email protected]>
1 parent 889302b commit cf2fceb

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

apps/platform/trpc/routers/orgRouter/users/invitesRouter.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,90 @@ export const invitesRouter = router({
317317
};
318318
}),
319319

320+
deleteInvite: orgAdminProcedure
321+
.input(
322+
z.object({
323+
invitePublicId: typeIdValidator('orgInvitations'),
324+
orgMemberPublicId: typeIdValidator('orgMembers'),
325+
emailIdentitiesPublicId: typeIdValidator('emailIdentities').optional()
326+
})
327+
)
328+
.mutation(async ({ ctx, input }) => {
329+
const { db } = ctx;
330+
const { orgMemberPublicId } = input;
331+
332+
const orgMember = await db.query.orgMembers.findFirst({
333+
where: eq(orgMembers.publicId, orgMemberPublicId),
334+
columns: {
335+
id: true,
336+
orgMemberProfileId: true,
337+
personalSpaceId: true
338+
}
339+
});
340+
341+
if (!orgMember) {
342+
throw new TRPCError({
343+
code: 'NOT_FOUND',
344+
message: 'Org Member not found'
345+
});
346+
}
347+
348+
const orgInvitesResponse = await db.query.orgInvitations.findFirst({
349+
where: eq(orgInvitations.orgMemberId, orgMember.id),
350+
columns: {
351+
id: true,
352+
acceptedAt: true
353+
}
354+
});
355+
356+
if (!orgInvitesResponse) {
357+
throw new TRPCError({
358+
code: 'NOT_FOUND',
359+
message: 'Invitation not found'
360+
});
361+
}
362+
363+
if (orgInvitesResponse.acceptedAt) {
364+
throw new TRPCError({
365+
code: 'FORBIDDEN',
366+
message: 'Used invitation cannot be deleted'
367+
});
368+
}
369+
370+
const {
371+
id: orgMemberId,
372+
orgMemberProfileId,
373+
personalSpaceId
374+
} = orgMember;
375+
376+
await db.transaction(async (db) => {
377+
if (input.emailIdentitiesPublicId) {
378+
await db
379+
.delete(emailIdentities)
380+
.where(eq(emailIdentities.publicId, input.emailIdentitiesPublicId));
381+
}
382+
383+
if (personalSpaceId) {
384+
await db
385+
.delete(spaceMembers)
386+
.where(eq(spaceMembers.spaceId, personalSpaceId));
387+
await db.delete(spaces).where(eq(spaces.id, personalSpaceId));
388+
}
389+
390+
await db
391+
.delete(orgMemberProfiles)
392+
.where(eq(orgMemberProfiles.id, orgMemberProfileId));
393+
394+
await db.delete(orgMembers).where(eq(orgMembers.id, orgMemberId));
395+
396+
if (orgInvitesResponse) {
397+
await db
398+
.delete(orgInvitations)
399+
.where(eq(orgInvitations.id, orgInvitesResponse.id));
400+
}
401+
});
402+
}),
403+
320404
validateInvite: publicProcedure
321405
.use(ratelimiter({ limit: 10, namespace: 'invite.validate' }))
322406
.input(

apps/web/src/app/[orgShortcode]/settings/org/users/invites/_components/columns.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
'use client';
22

33
import { createColumnHelper, type ColumnDef } from '@tanstack/react-table';
4+
import { Button } from '@/src/components/shadcn-ui/button';
45
import { CopyButton } from '@/src/components/copy-button';
56
import { Badge } from '@/src/components/shadcn-ui/badge';
7+
import { useOrgShortcode } from '@/src/hooks/use-params';
68
import type { RouterOutputs } from '@/src/lib/trpc';
79
import { Avatar } from '@/src/components/avatar';
10+
import { Trash } from '@phosphor-icons/react';
11+
import { platform } from '@/src/lib/trpc';
12+
import { cn } from '@/src/lib/utils';
813
import { format } from 'date-fns';
914
import { env } from '@/src/env';
15+
import { toast } from 'sonner';
1016

1117
type Member =
1218
RouterOutputs['org']['users']['invites']['viewInvites']['invites'][number];
@@ -129,5 +135,50 @@ export const columns: ColumnDef<Member>[] = [
129135
</div>
130136
) : null;
131137
}
138+
}),
139+
columnHelper.display({
140+
id: 'delete',
141+
header: '',
142+
143+
cell: ({ row }) => <DeleteInviteCell row={row.original} />
132144
})
133145
];
146+
147+
const DeleteInviteCell: React.FC<{ row: Member }> = ({ row }) => {
148+
const orgShortcode = useOrgShortcode();
149+
const utils = platform.useUtils();
150+
151+
const { mutate: deleteInvite } =
152+
platform.org.users.invites.deleteInvite.useMutation({
153+
onSuccess: () => {
154+
toast.success('Invitation deleted');
155+
void utils.org.users.invites.viewInvites.refetch();
156+
}
157+
});
158+
159+
const handleDelete = () => {
160+
const invitePublicId = row.publicId as string;
161+
const orgMemberPublicId = row.orgMember?.publicId as string;
162+
163+
deleteInvite({
164+
orgShortcode,
165+
invitePublicId,
166+
orgMemberPublicId
167+
});
168+
};
169+
170+
return (
171+
<div className="flex h-full w-full items-center">
172+
<Button
173+
onClick={handleDelete}
174+
variant={'ghost'}
175+
size="icon"
176+
className={cn(
177+
'bg-red-4 hover:bg-red-5 m-1 uppercase',
178+
row.acceptedAt ? 'hidden' : 'flex'
179+
)}>
180+
<Trash className="fill-white" />
181+
</Button>
182+
</div>
183+
);
184+
};

0 commit comments

Comments
 (0)