diff --git a/apps/builder/src/features/auth/api/convertInvitationsToCollaborations.ts b/apps/builder/src/features/auth/api/convertInvitationsToCollaborations.ts index f42c92fb9..63e0b4e69 100644 --- a/apps/builder/src/features/auth/api/convertInvitationsToCollaborations.ts +++ b/apps/builder/src/features/auth/api/convertInvitationsToCollaborations.ts @@ -29,19 +29,15 @@ export const convertInvitationsToCollaborations = async ( ) for (const invitation of workspaceInvitations) { if (!invitation.typebot.workspaceId) continue - await p.memberInWorkspace.upsert({ - where: { - userId_workspaceId: { + await p.memberInWorkspace.createMany({ + data: [ + { userId: id, workspaceId: invitation.typebot.workspaceId, + role: WorkspaceRole.GUEST, }, - }, - create: { - userId: id, - workspaceId: invitation.typebot.workspaceId, - role: WorkspaceRole.GUEST, - }, - update: {}, + ], + skipDuplicates: true, }) } return p.invitation.deleteMany({ diff --git a/apps/builder/src/features/auth/api/joinWorkspaces.ts b/apps/builder/src/features/auth/api/joinWorkspaces.ts index cbaebca74..b51e4e540 100644 --- a/apps/builder/src/features/auth/api/joinWorkspaces.ts +++ b/apps/builder/src/features/auth/api/joinWorkspaces.ts @@ -12,6 +12,7 @@ export const joinWorkspaces = async ( role: invitation.type, userId: id, })), + skipDuplicates: true, }), p.workspaceInvitation.deleteMany({ where: { diff --git a/apps/builder/src/features/workspace/components/MembersList/MembersList.tsx b/apps/builder/src/features/workspace/components/MembersList/MembersList.tsx index 8433336b0..ffbaa63d7 100644 --- a/apps/builder/src/features/workspace/components/MembersList/MembersList.tsx +++ b/apps/builder/src/features/workspace/components/MembersList/MembersList.tsx @@ -8,9 +8,8 @@ import { import { UnlockPlanAlertInfo } from '@/components/UnlockPlanAlertInfo' import { WorkspaceInvitation, WorkspaceRole } from 'db' import React from 'react' -import { getSeatsLimit } from 'utils/pricing' +import { getSeatsLimit, isSeatsLimitReached } from 'utils/pricing' import { AddMemberForm } from './AddMemberForm' -import { checkCanInviteMember } from './helpers' import { MemberItem } from './MemberItem' import { useUser } from '@/features/account' import { useWorkspace } from '../../WorkspaceProvider' @@ -20,6 +19,7 @@ import { updateMemberQuery } from '../../queries/updateMemberQuery' import { deleteInvitationQuery } from '../../queries/deleteInvitationQuery' import { updateInvitationQuery } from '../../queries/updateInvitationQuery' import { Member } from '../../types' +import { isDefined } from 'utils' export const MembersList = () => { const { user } = useUser() @@ -88,11 +88,16 @@ export const MembersList = () => { members.filter((member) => member.role !== WorkspaceRole.GUEST).length + invitations.length - const canInviteNewMember = checkCanInviteMember({ - plan: workspace?.plan, - customSeatsLimit: workspace?.customSeatsLimit, - currentMembersCount, - }) + const seatsLimit = workspace ? getSeatsLimit(workspace) : undefined + + const canInviteNewMember = + workspace && + !isSeatsLimitReached({ + plan: workspace?.plan, + customSeatsLimit: workspace?.customSeatsLimit, + existingMembersCount: currentMembersCount, + existingInvitationsCount: invitations.length, + }) return ( @@ -104,12 +109,12 @@ export const MembersList = () => { `} /> )} - {workspace && ( + {isDefined(seatsLimit) && ( Members{' '} - {getSeatsLimit(workspace) === -1 + {seatsLimit === -1 ? '' - : `(${currentMembersCount}/${getSeatsLimit(workspace)})`} + : `(${currentMembersCount + invitations.length}/${seatsLimit})`} )} {workspace?.id && canEdit && ( diff --git a/apps/builder/src/features/workspace/components/MembersList/helpers.ts b/apps/builder/src/features/workspace/components/MembersList/helpers.ts deleted file mode 100644 index c75f92fd7..000000000 --- a/apps/builder/src/features/workspace/components/MembersList/helpers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Plan } from 'db' -import { getSeatsLimit } from 'utils/pricing' - -export function checkCanInviteMember({ - plan, - customSeatsLimit, - currentMembersCount, -}: { - plan?: Plan - customSeatsLimit?: number | null - currentMembersCount?: number -}) { - if (!plan || !currentMembersCount) return false - - const seatsLimit = getSeatsLimit({ - plan, - customSeatsLimit: customSeatsLimit ?? null, - }) - - if (seatsLimit === -1) return true - - return seatsLimit > currentMembersCount -} diff --git a/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts b/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts index bfb50f291..1fed19f43 100644 --- a/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts +++ b/apps/builder/src/pages/api/workspaces/[workspaceId]/invitations.ts @@ -1,11 +1,11 @@ -import { Workspace, WorkspaceInvitation, WorkspaceRole } from 'db' +import { WorkspaceInvitation, WorkspaceRole } from 'db' import prisma from '@/lib/prisma' import { NextApiRequest, NextApiResponse } from 'next' import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api' import { getAuthenticatedUser } from '@/features/auth/api' -import { getSeatsLimit } from 'utils/pricing' import { sendWorkspaceMemberInvitationEmail } from 'emails' import { env } from 'utils' +import { isSeatsLimitReached } from 'utils/pricing' const handler = async (req: NextApiRequest, res: NextApiResponse) => { const user = await getAuthenticatedUser(req) @@ -23,7 +23,22 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }) if (!workspace) return forbidden(res) - if (await checkIfSeatsLimitReached(workspace)) + const [existingMembersCount, existingInvitationsCount] = + await prisma.$transaction([ + prisma.memberInWorkspace.count({ + where: { workspaceId: workspace.id }, + }), + prisma.workspaceInvitation.count({ + where: { workspaceId: workspace.id }, + }), + ]) + if ( + isSeatsLimitReached({ + existingMembersCount, + existingInvitationsCount, + ...workspace, + }) + ) return res.status(400).send('Seats limit reached') if (existingUser) { await prisma.memberInWorkspace.create({ @@ -66,12 +81,4 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { methodNotAllowed(res) } -const checkIfSeatsLimitReached = async (workspace: Workspace) => { - const existingMembersCount = await prisma.memberInWorkspace.count({ - where: { workspaceId: workspace.id }, - }) - - return existingMembersCount >= getSeatsLimit(workspace) -} - export default handler diff --git a/packages/utils/pricing.ts b/packages/utils/pricing.ts index f91255703..fb8dcc3cb 100644 --- a/packages/utils/pricing.ts +++ b/packages/utils/pricing.ts @@ -120,6 +120,22 @@ export const getSeatsLimit = ({ return seatsLimit[plan].totalIncluded } +export const isSeatsLimitReached = ({ + existingMembersCount, + existingInvitationsCount, + plan, + customSeatsLimit, +}: { existingMembersCount: number; existingInvitationsCount: number } & Pick< + Workspace, + 'plan' | 'customSeatsLimit' +>) => { + const seatsLimit = getSeatsLimit({ plan, customSeatsLimit }) + return ( + seatsLimit !== infinity && + seatsLimit <= existingMembersCount + existingInvitationsCount + ) +} + export const computePrice = ( plan: Plan, selectedTotalChatsIndex: number,