Files
supabase/apps/studio/data/organization-members/organization-invitation-create-mutation.ts
Samir Ketema 8454ec241d feat: add batch email org invites (#44832)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature

## What is the current behavior?

When sending organization invites to multiple emails at once, the
invitations API is called once for each email passed, passing a single
email address in the `email` field.

## What is the new behavior?

A single request is used when sending multiple organization invites at
once, by using the new `emails` field.

## Additional context

This builds further on https://github.com/supabase/supabase/pull/42637

⚠️ Note: I'd like to merge this after getting the API changes in first:
https://github.com/supabase/platform/pull/31561


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Bulk invite: paste comma-separated emails (parsed, trimmed,
deduplicated, lowercased) and send as a single batched request; inputs
are categorized into new, already-invited, and existing members.
  * SSO and project scope options included in invite payloads.

* **Bug Fixes / API**
* Invitation endpoint now accepts multiple emails; resend uses
multi-email format. Invalid addresses are blocked, existing members are
skipped with error toasts, and overall success is reported with the
dialog closing after invite.

* **Tests**
* Added unit and UI tests covering parsing, categorization, payload
building, validation limits, and invite flows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
2026-04-23 18:02:08 +00:00

80 lines
2.3 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { components } from 'api-types'
import { toast } from 'sonner'
import { organizationKeys } from './keys'
import { handleError, post } from '@/data/fetchers'
import { organizationKeys as organizationKeysV1 } from '@/data/organizations/keys'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type OrganizationCreateInvitationVariables = {
slug: string
emails: string[]
roleId: number
projects?: string[]
requireSso?: boolean
}
export async function createOrganizationInvitation({
slug,
emails,
roleId,
projects,
requireSso,
}: OrganizationCreateInvitationVariables) {
const payload: components['schemas']['CreateInvitationBody'] = { emails, role_id: roleId }
if (projects !== undefined) payload.role_scoped_projects = projects
if (requireSso !== undefined) payload.require_sso = requireSso
const { data, error } = await post('/platform/organizations/{slug}/members/invitations', {
params: { path: { slug } },
body: payload,
})
if (error) handleError(error)
return data
}
type OrganizationMemberUpdateData = Awaited<ReturnType<typeof createOrganizationInvitation>>
export const useOrganizationCreateInvitationMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<
OrganizationMemberUpdateData,
ResponseError,
OrganizationCreateInvitationVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
OrganizationMemberUpdateData,
ResponseError,
OrganizationCreateInvitationVariables
>({
mutationFn: (vars) => createOrganizationInvitation(vars),
async onSuccess(data, variables, context) {
const { slug } = variables
await Promise.all([
queryClient.invalidateQueries({ queryKey: organizationKeys.rolesV2(slug) }),
queryClient.invalidateQueries({ queryKey: organizationKeysV1.members(slug) }),
])
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to send invitation${data.message ? ': ' + data.message : ''}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}